CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
OAuth 2.0 与 OpenID Connect/课时/常见安全问题
RawBlame
课时目录12 节

OAuth 2.0 与 OpenID Connect

  1. 01OAuth 是什么
  2. 02授权码流
  3. 03PKCE 与 SPA
  4. 04OpenID Connect
  5. 05Scope 与权限
  6. 06Refresh Token
  7. 07Spring OAuth2 Client
  8. 08Resource Server
  9. 09第三方登录
  10. 10OAuth vs 本站 JWT 登录
  11. 11常见安全问题
  12. 12综合练习

常见安全问题

redirect_uri 校验、token 泄露、implicit 废弃。

30 分钟

常见安全问题

这一课你将能做到

  1. 拿着清单逐项自查:state、PKCE、redirect_uri 白名单、HTTPS、secret 存放、token 存储与日志脱敏
  2. 说清 Implicit 与 Resource Owner Password 两种模式被废弃的原因,能在代码评审时一票否决
  3. 制定 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」处理,绝不降级放行

讲清楚要点

  1. callback 可被诱导触发,所以 state 必须不可预测且与会话绑定:校验通过后立即删除,防同一 state 复用。
  2. 公共客户端无 secret,PKCE 是必选项而非可选优化:S256 是底线,plain 仅调试。
  3. 开放重定向能把 code 送到攻击者手里:redirect_uri 全等匹配 + 业务跳转参数白名单,两道闸都要关。
  4. secret 等于 Client 的身份:前端包、移动安装包、截图、CI 日志都是泄露渠道;轮换预案要在泄露前写好。
  5. 密码模式与 Implicit 被废弃是生态共识:评审时直接引用本课对照表否决,不需要重新辩论。
  6. 防御是分层的: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_ 或提交仓库任何人可冒充你的应用环境变量注入 + 扫描 + 轮换预案

卡点急救

  1. 现象:怀疑 secret 已泄露。三步:① 立即在 IdP 后台轮换 secret(GitHub/Google 都支持);② 撤销当前所有 refresh(或删除服务端库存),强制重新授权;③ 排查泄露渠道(git 历史、CI 日志、截图)并堵住。git 历史里已提交的 secret 用工具清除后仍必须轮换——视为已泄露。
  2. 现象:用户反馈「没操作却被登录成了别人」。典型 login CSRF:攻击者把带自己 code 的 callback 链接发给了用户。检查 state 校验是否真的在执行(尤其是否被「临时禁用调试」后忘了恢复);确认 state 是会话绑定的一次性随机值。
  3. 现象:日志平台里出现大量完整 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。

自测三问

  1. state 与 PKCE 分别挡住哪类攻击?为什么两个都要?
  2. 举两个危险的 redirect_uri 配置习惯,并给出修正。
  3. 发现 refresh 可能泄露时,应急三步是什么?

小结一句话

安全 OAuth = 正确授权流 + 严格 redirect 白名单 + state/PKCE + 短 token 可撤销 + secret 永不进前端——上线前清单逐项打勾,泄露时按预案三步走。

延伸

  • OAuth 2.0 Security Best Current Practice(IETF)
  • oauth.net:OAuth 2.1 最佳实践方向
  • 下一课:综合练习——把 11 课的零件装成一台能演示、能回归、能过安全门禁的整机。
加载本节练习…
← 上一课OAuth vs 本站 JWT 登录下一课 →综合练习