土法炼钢兴趣小组的算法知识备份

【身份与访问控制工程】OAuth 2.1 与 PKCE:现代授权主路径

文章导航

分类入口
architecturesecurity
标签入口
#oauth2#oauth21#pkce#dpop#authorization-code#refresh-token#rar#dcr#security

目录

OAuth 2.0(RFC 6749)发布于 2012 年。此后十二年间,安全研究发现了一系列攻击向量——授权码拦截、CSRF 状态泄露、Refresh Token 窃取——每次发现都催生一个补充 RFC。到 2024 年,OAuth 2.1 将这些安全增强整合为一份连贯规范(draft-ietf-oauth-v2-1-11),做了一次”OAuth 2.0 安全最佳实践的底线收敛”。

对工程团队来说,理解 OAuth 2.1 意味着把十几份分散的 RFC(PKCE 的 RFC 7636、Token Binding 的 RFC 8705、Refresh Token Rotation 的 RFC 7009 等)合并成一套统一的安全配置。本文以”攻击→防御”为主线,拆解 OAuth 2.1 的精简授权码流程全栈。

一、OAuth 2.1 删掉了什么

OAuth 2.1 最直观的变化是删除

已删除 原因 替代方案
Implicit Grant (response_type=token) access_token 直接暴露在 URL fragment 中,可被 Referer Header、浏览器历史、JavaScript 读取 Authorization Code + PKCE
Resource Owner Password Grant (grant_type=password) 用户凭据直接交给第三方应用,违反”不共享密码”原则 Authorization Code + PKCE,或 Client Credentials(服务间)
Bearer Token 在 URL query string 中的传输 URL 出现在 browser history、Referer Header、代理日志中 Authorization Header (Bearer + token) 或 POST body (application/x-www-form-urlencoded)

这意味着 OAuth 2.1 的唯一推荐授权流程是 Authorization Code Grant + PKCE。对于服务间通信,保留 Client Credentials Grant。对于设备(如智能电视、IoT),保留 Device Authorization Grant(RFC 8628)。

二、PKCE 不是可选的

PKCE(Proof Key for Code Exchange,RFC 7636)的原始动机是解决公开客户端(如移动 App)的授权码拦截问题。OAuth 2.1 将其要求扩展到所有客户端——包括机密客户端(后端服务器)。

2.1 攻击模型:为什么没有 PKCE 会出事

用户在浏览器上授权后,OP 将 authorization_code 附加在 redirect_uri 上返回给 RP。问题在于:code 经过浏览器地址栏——攻击者可以通过多种方式截获它:

PKCE 的防御逻辑:在授权请求时 RP 发送 code_challengecode_verifier 的 SHA-256 哈希),在 /token 端点再发送 code_verifier 原文——OP 哈希后比对。攻击者即便截获了 code,没有 code_verifier 也无法换取 access_token

2.2 PKCE 的密码学细节

sequenceDiagram
    participant RP as 应用 (RP)
    participant User as 浏览器
    participant OP as 授权服务器 (OP)

    Note over RP: 1. 生成 code_verifier
    Note over RP: code_verifier = 43-128 chars<br/>随机字符串 [A-Za-z0-9._~-]
    Note over RP: 2. 计算 code_challenge
    Note over RP: code_challenge = BASE64URL(SHA256(code_verifier))

    RP->>User: 3. GET /authorize?code_challenge=XYZ&code_challenge_method=S256
    User->>OP: 4. 浏览器跳转到 OP
    OP->>User: 5. 用户认证并授权
    OP->>User: 6. 302 redirect_uri?code=AUTH_CODE

    RP->>OP: 7. POST /token (code + code_verifier)
    Note over OP: 8. 验证:SHA256(code_verifier) == code_challenge
    OP->>RP: 9. { access_token, ... }

关键参数:

工程陷阱 1code_challenge_method=plain 不提供任何保护——攻击者截获 code 的同时也截获了 code_challengecode_challenge 就是 code_verifier 原文),可以直接用来请求 /token。OAuth 2.1 要求 OP 拒绝 code_challenge_method=plain

2.3 为什么机密客户端也需要 PKCE

后端服务器可以安全存储 client_secret,但 code 仍然经过浏览器。攻击者可以截获 code 后,赶在合法客户端之前用 code 请求 /token(authorization code replay)。PKCE 的 code_verifier 只在 RP 后端持有,不经过浏览器——攻击者没有它就无法完成 code 兑换。这为机密客户端额外增加了一层纵深防御。

三、Refresh Token 的安全加固

OAuth 2.0 的 Refresh Token 设计有一个根本问题:Token 是 Bearer Token(持有者令牌),谁持有它就能用它。如果 Refresh Token 被泄露——通过网络嗅探、日志打印、数据库漏扫——攻击者可以持续获取新的 Access Token,直到 Refresh Token 过期或被吊销。

3.1 Refresh Token Rotation(轮换)

OAuth 2.1 强制要求:每次用 Refresh Token 换新 Access Token 时,OP 必须同时签发新的 Refresh Token,并立即使旧 Refresh Token 失效。

原 Refresh Token (R1)
  → POST /token grant_type=refresh_token refresh_token=R1
  → 返回 { access_token: A2, refresh_token: R2 }
  → R1 立即失效,R2 成为唯一有效 Refresh Token

如果攻击者持有 R1 在合法客户端之前使用:
  → 攻击者用 R1 拿到 A_attacker + R3
  → R1 失效,合法客户端的 R1 不再可用
  → 合法客户端发现 R1 失败 → 告警 → 触发安全响应

如果合法客户端先用 R1:
  → 攻击者的 R1 已经失效 → 攻击失败

这个机制不能阻止 Refresh Token 泄露,但能检测泄露:如果合法客户端使用的 Refresh Token 突然失效,说明有人已经用了它。

3.2 Sender-Constrained Token(发送者约束令牌)

Token 轮换解决了”检测泄露”的问题,但没解决”防止泄露后被使用”。Sender-Constrained Token 把令牌绑定到特定的客户端实例,即使令牌被窃取,攻击者也无法使用。

两种主要机制:

  1. mTLS(RFC 8705):令牌绑定到客户端的 TLS 证书。/token 请求时,OP 验证客户端证书并将 Token 绑定到该证书。后续使用 Token 访问资源时,客户端必须出示相同的 TLS 证书。要求部署 PKI 基础设施。

  2. DPoP(Demonstration of Proof-of-Possession,RFC 9449):应用层方案。客户端生成非对称密钥对,在每次请求时用私钥签名一个 DPoP Proof(JWT 格式),证明”持有 Token 的人和生成 DPoP 密钥对的人是同一个”。不需要 PKI,比 mTLS 更轻量。

DPoP 的核心机制:

客户端生成 key pair(一次,跨会话保持)
  ↓
POST /token 附带 DPoP proof:
  { typ: "dpop+jwt", alg: "ES256", jwk: { 公钥 } }
  { jti: "unique-nonce", htm: "POST", htu: "https://op.example.com/token", iat: ... }

  ↓ OP 验证 DPoP proof → 签发 access_token,绑定到 jwk thumbprint

后续 API 调用:
  Authorization: DPoP eyJhbG...access_token
  DPoP: eyJhbG...(新的 DPoP proof, 相同 jwk, htm/htu 匹配当前请求)

DPoP 是一个相对新的 RFC(2024 年发布),主流 OP 的支持情况不一致。截至 2026 年初,Auth0 和 Microsoft Entra ID 已支持 DPoP,Keycloak 在 24.x 版本中实验性支持。选择 DPoP 时需要评估 OP 的支持情况和客户端的实现成本。

四、精确 Redirect URI 比对与 state 参数

OAuth 2.0 允许 OP 使用”前缀匹配”(prefix matching)来验证 redirect_uri——如果注册的是 https://app.example.com/callbackhttps://app.example.com/callback/attacker 也可能被接受。这导致攻击者可以构造一个指向恶意路径的回调 URL。

OAuth 2.1 要求精确匹配(exact string matching),除非注册的 URI 本身就是一个允许子路径的模式(如 https://app.example.com/callback/*)。

state 参数的作用是绑定授权请求和回调响应——RP 生成随机 state,OP 原样返回,RP 验证一致性。这防止了 CSRF 攻击(攻击者诱导用户点击一个构造的 OAuth 回调 URL,让用户的账户绑定到攻击者的第三方账户)。

工程陷阱 2state 不是 noncestate 绑定授权请求和回调,nonce 绑定授权请求和 ID Token。两者都必须在每次授权请求中生成新值。

五、RAR(Rich Authorization Requests):超越 scope

OAuth 2.0 的 scope 机制过于粗糙——“允许这个 scope 就能做这一类操作”。实际业务需要在授权时表达精细的约束:“只允许对这 3 个仓库的读权限”“只允许转账不超过 1000 元”“只在工作时间允许访问”。

RAR(RFC 9396)引入 authorization_details 参数,用 JSON 结构替代扁平的 scope 字符串:

{
  "authorization_details": [
    {
      "type": "account_information",
      "actions": ["read"],
      "locations": ["https://api.example.com/accounts/123"]
    },
    {
      "type": "payment_initiation",
      "actions": ["create"],
      "locations": ["https://api.example.com/payments"],
      "max_amount": { "value": 1000, "currency": "CNY" }
    }
  ]
}

RAR 的典型应用场景:

RAR 在实际落地中受限于 OP 和资源服务器的支持。截至 2026 年,Auth0 和 Okta 支持 authorization_details,Keycloak 在 24.x 版本中有限支持。

六、小结:OAuth 2.1 的”底线配置”

如果你要新部署一个 OAuth 授权系统,以下配置应该作为默认起点(除非有明确理由偏离):

  1. 只使用 Authorization Code Grant + PKCEcode_challenge_method=S256
  2. 所有客户端都使用 PKCE,包括机密客户端。
  3. Refresh Token Rotation(每次换新 token 时发新的 refresh token,旧的立即失效)。
  4. Sender-Constrained Token(有条件的团队使用 DPoP,有 PKI 基础设施的用 mTLS)。
  5. 精确 redirect_uri 匹配
  6. 对所有客户端验证 state 参数
  7. SPA 使用 BFF(Backend for Frontend)架构,不在浏览器端直接处理 token。

上一篇企业单点登录:OIDC 与现代 SSO 下一篇SAML 还值得学吗:企业遗留 SSO 的现实世界

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-06-13 · architecture / security

【身份与访问控制工程】企业单点登录:OIDC 与现代 SSO

OIDC 是当下企业 SSO 的事实标准,但大多数实现只用了它 20% 的规范。本文从 OIDC 核心规范出发,拆解 Authorization Code Flow + PKCE 的完整交互、ID Token 的验证规则、Discovery 与 Dynamic Registration 的互操作性机制,以及 RP-Initiated Logout 和 Session Management 的工程实现细节。

2026-06-13 · architecture / security

【身份与访问控制工程】IAM 全景:为什么这是高价值赛道

从 2020 年 SolarWinds 到 2024 年 Okta 支持系统泄露,身份基础设施的安全失败反复证明一件事:IAM 不是 IT 支撑系统,而是安全架构的承重墙。本文建立现代 IAM 的全景地图——从认证协议、令牌体系、权限模型到身份治理与平台选型,给出 5 个贯穿全系列的核心问题。

2026-06-15 · architecture / security

【身份与访问控制工程】Session、Refresh Token 与吊销体系

JWT 的无状态设计带来了可扩展性,但让令牌吊销变成了系统性问题——签出去的 JWT 在到期之前全是活令牌。Refresh Token Rotation、Token Introspection、基于事件的吊销通知、撤销列表——这些机制构成了身份系统的'紧急刹车',各自的成本、延迟和覆盖范围完全不同。本文拆解四种吊销机制的工程权衡。

2026-06-17 · architecture / security

【身份与访问控制工程】服务身份:mTLS、SPIFFE/SPIRE 与 Workload Identity

前 9 篇讨论的都是'人'的身份——用户怎么登录、怎么验证。但微服务世界中,80% 的 API 调用是服务之间的。服务身份(Workload Identity)是整个 IAM 体系的另一半:mTLS 解决'传输层你是谁',SPIFFE/SPIRE 解决'在平台层你是谁且怎么证明',JWT Profile for OAuth 解决'我怎么拿到一个服务身份的 Token'。本文从这三条线拆解服务身份的工程实现。


By .