大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
常见安全问题
这一课你将能做到
- 拿着清单逐项自查:state、PKCE、redirect_uri 白名单、HTTPS、secret 存放、token 存储与日志脱敏
- 说清 Implicit 与 Resource Owner Password 两种模式被废弃的原因,能在代码评审时一票否决
- 制定 token 泄露应急预案:识别 → 轮换 secret / 撤销 refresh → 强制重新授权 → 复盘日志
为什么要学
OAuth 配错的后果不是「登录按钮没反应」,而是 账号劫持、开放重定向、token 盗用 这类能上新闻的事故。绝大多数事故的根因是实现偷懒:
- state 固定成字符串「xyz」→ CSRF 防线归零;
- redirect_uri 用前缀匹配甚至通配 → code 被送到攻击者站点;
- client_secret 写进前端环境变量 → 机密公开;
- access_token 有效期 30 天且无撤销 → 一次泄露 = 一个月敞开的大门。
本课把前面 10 课散落的安全点收拢成一份上线门禁清单:联邦登录或开放 API 上线前,逐项打勾,勾不完不放行。
先建立图像
攻击面 防护锁
──────────────────────────────────────────────
伪造 callback(CSRF) → state:随机 + 会话绑定 + 一次性
截获 code 换票 → PKCE(公共客户端)/ client_secret(机密客户端)
偷换回调地址 → redirect_uri 精确白名单 + 禁开放重定向
窃听传输 → 全程 HTTPS(生产)
XSS 偷凭证 → HttpOnly cookie + 凭证不进 JS 可读存储 + CSP
scope 过大 → 最小权限申请 + API 端强制校验
凭证长寿 → 短 access + refresh 轮换 + 撤销端点
secret 泄露 → 环境变量/密钥管理 + 仓库扫描 + 轮换预案
每一行都是前面某课的一个知识点——安全不是一门新学问,而是把已学的纪律逐条落实。
跟着做
步骤 1:审查 redirect_uri 白名单
✅ 合法示例:
https://app.example.com/oauth/callback
⚠️ 危险倾向:
https://app.example.com/oauth/callback.evil.com (若匹配逻辑是前缀)
https://evil.com/…?next=https://app.example.com/… (借开放重定向中转)
允许通配 * 或任意追加 query (等于没有白名单)
规则:IdP 控制台只保留精确 URI(全等比较,含协议、域名、端口、路径);本地与生产分两个 OAuth App;代码里跳转逻辑禁止接收任意 URL 参数作为 redirect 目标。
步骤 2:确认 state 与 PKCE 同时启用(门禁伪码)
// 中文注释:上线前对照——两个防护缺一不可
void startLogin(HttpSession session) {
session.setAttribute("oauth_state", randomToken()); // 随机 + 一次性
session.setAttribute("pkce_verifier", randomVerifier()); // 公共客户端必配
}
void onCallback(String state, String code, HttpSession session) {
requireEquals(state, session.take("oauth_state")); // 校验后立即删除
// 换 token 必须带 verifier(公共)或 client_secret(机密)
}
步骤 3:扫描仓库里是否泄露 secret
# 中文注释:仓库根目录自查;发现真实值立即到 IdP 后台轮换 secret
rg -n "GITHUB_CLIENT_SECRET|client-secret:\s*['\"]?[A-Za-z0-9]" \
--glob '!**/node_modules/**' --glob '!**/target/**'
# 前端项目再查一遍「公开前缀」环境变量里是否混进了机密
rg -n "NEXT_PUBLIC_.*(SECRET|SECRET_KEY|CLIENT_SECRET)" .
正确形态:仓库里只有 ${GITHUB_CLIENT_SECRET} 占位与 .env.example 示例;真实值走本地 .env(gitignore)与 CI/生产的密钥注入。
步骤 4:把废弃模式对照表抄进团队文档
❌ Implicit —— token 在 URL fragment,暴露面过大
❌ Resource Owner Password —— 用户密码交给 Client,教坏整个生态
❌ 长寿命 access 无撤销 —— 泄露窗口以「天」计
✅ Authorization Code + PKCE(浏览器/移动)
✅ 机密 Client 授权码 + secret(有后端)
✅ Client Credentials(纯机器,无终端用户)
步骤 5:token 存储与日志演练
前端:本站会话用 HttpOnly + Secure + SameSite cookie;绝不 localStorage 存 IdP refresh
后端:refresh 加密存 Redis/DB;日志中 token 一律打码 → eyJ***abc 或只记 jti
演练:轮换 IdP secret → 撤销全部 refresh → 观察用户被要求重新授权 → 恢复正常
步骤 6:JWT 断言门禁(RS 侧)
拒绝:alg=none、未知 iss、aud 不符、已过期、JWKS 里找不到 kid
保持:Spring 默认校验链开启(exp/iss/签名),aud 显式加校验
原则:任何一项校验失败都按「未认证 401」处理,绝不降级放行
讲清楚要点
- callback 可被诱导触发,所以 state 必须不可预测且与会话绑定:校验通过后立即删除,防同一 state 复用。
- 公共客户端无 secret,PKCE 是必选项而非可选优化:S256 是底线,plain 仅调试。
- 开放重定向能把 code 送到攻击者手里:redirect_uri 全等匹配 + 业务跳转参数白名单,两道闸都要关。
- secret 等于 Client 的身份:前端包、移动安装包、截图、CI 日志都是泄露渠道;轮换预案要在泄露前写好。
- 密码模式与 Implicit 被废弃是生态共识:评审时直接引用本课对照表否决,不需要重新辩论。
- 防御是分层的:PKCE 不挡 XSS,短 token 不替代 HTTPS,框架默认不替代清单自查——任何一层失守,其他层要能兜住。
你可能会懵的地方
-
开发环境用 http://localhost 不安全吗? 本地惯例可接受(IdP 对 localhost 有例外放行);但宽松规则绝不能抄进生产配置,生产一律 HTTPS。
-
CORS 配
*再带上 cookie 行不行? 浏览器规范直接拦截(credentials 模式下不允许通配源)。就算勉强绕过,也只是把盗用面扩大。OAuth callback 是顶级导航,与 API CORS 是两个问题,分开治理。 -
state 需要加密携带业务数据吗? 不需要。state 就是不可预测随机串;业务数据放服务端(session/Redis),用 state 当 key 关联。把数据塞 state 既危险又多此一举。
-
用了 Spring 默认 oauth2Login 是不是就自动安全了? 框架解决了 state、code 交换等协议层问题,但以下仍归你管:redirect 注册是否精确、secret 是否泄露、成功处理器里是否把 token 拼进不安全 URL、日志是否打码。
-
access_token 设 15 分钟,用户体验会不会很差? 不会——有 refresh 轮换(或本站的滑动续期)兜底,用户感知不到。真正体验差的是「为了省事设 30 天」然后某天全体强制改密。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
state 固定为 xyz 或跨请求复用 | CSRF 防线归零,login CSRF 畅通 | 每次登录生成新随机 state,校验后删除 |
| redirect 用前缀匹配/通配符 | callback.evil.com 这类都能过 | 精确全等匹配白名单 |
| Implicit「先上线再说」 | token 暴露在 URL,无 refresh | 授权码 + PKCE |
| access 30 天 + refresh 永不过期 | 泄露窗口无限大 | 短 access + 可轮换可撤销 refresh |
| 日志 INFO 级打印完整 Bearer | 日志即泄露源 | 打码或只记 jti/hash |
| secret 写进 NEXT_PUBLIC_ 或提交仓库 | 任何人可冒充你的应用 | 环境变量注入 + 扫描 + 轮换预案 |
卡点急救
- 现象:怀疑 secret 已泄露。三步:① 立即在 IdP 后台轮换 secret(GitHub/Google 都支持);② 撤销当前所有 refresh(或删除服务端库存),强制重新授权;③ 排查泄露渠道(git 历史、CI 日志、截图)并堵住。git 历史里已提交的 secret 用工具清除后仍必须轮换——视为已泄露。
- 现象:用户反馈「没操作却被登录成了别人」。典型 login CSRF:攻击者把带自己 code 的 callback 链接发给了用户。检查 state 校验是否真的在执行(尤其是否被「临时禁用调试」后忘了恢复);确认 state 是会话绑定的一次性随机值。
- 现象:日志平台里出现大量完整 token。立即:打码规则补上(脱敏中间件统一处理);已入库的 token 视为泄露——轮换 secret、撤销 refresh、用户侧强制重登;复盘确认拦截器/异常栈不会把 header 原文打进日志。
面试常问
-
OAuth 里最常见的三类攻击与对应防线? ① CSRF / login CSRF → state(随机、绑定会话、一次性);② 授权码拦截 → PKCE(公共客户端)或 client_secret(机密客户端);③ 开放重定向 → redirect_uri 精确白名单。再加基础卫生:HTTPS、secret 管理、日志脱敏。
-
为什么 Implicit 和密码模式被废弃? Implicit 把 access_token 直接放进 URL fragment,被历史/插件/Referer 收集且无法用 refresh 轮换,风险不可控;密码模式让用户把密码交给第三方应用,破坏「密码只交给 IdP」的基本信任模型。OAuth 2.1 已将两者移除,统一为授权码 + PKCE。
-
发现 refresh token 泄露,你的应急动作是什么? 立即撤销该 refresh 及其派生 access(撤销端点/删 Redis),强制相关用户重新授权;轮换可能同批泄露的 secret;排查泄露路径(日志/前端存储/传输);若已启用 rotation,重放记录本身就是定位攻击者的线索;最后复盘写入 runbook。
自测三问
- state 与 PKCE 分别挡住哪类攻击?为什么两个都要?
- 举两个危险的 redirect_uri 配置习惯,并给出修正。
- 发现 refresh 可能泄露时,应急三步是什么?
小结一句话
安全 OAuth = 正确授权流 + 严格 redirect 白名单 + state/PKCE + 短 token 可撤销 + secret 永不进前端——上线前清单逐项打勾,泄露时按预案三步走。
延伸
- OAuth 2.0 Security Best Current Practice(IETF)
- oauth.net:OAuth 2.1 最佳实践方向
- 下一课:综合练习——把 11 课的零件装成一台能演示、能回归、能过安全门禁的整机。