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 最直观的变化是删除:
| 已删除 |
原因 |
替代方案 |
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
经过浏览器地址栏——攻击者可以通过多种方式截获它:
- 恶意 App 注册相同的自定义 URI
scheme:iOS/Android 上,多个 App 可以声明相同的
redirect_uri scheme。用户授权后,OS 不确定该把
redirect 交给哪个 App——恶意 App 抢先接收,拿到
code,换取 access_token。
- 浏览器扩展 / 恶意脚本:如果 RP 是
SPA,redirect 处理页面可能是攻击者可脚本注入的。
- Referer Header 泄露:如果 redirect
页面加载了第三方资源(图片、脚本),请求的 Referer Header
会携带完整 URL,包括
code 参数。
PKCE 的防御逻辑:在授权请求时 RP 发送
code_challenge(code_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, ... }
关键参数:
code_verifier:43 到 128
字符的高熵随机字符串,字符集
[A-Za-z0-9._~-](unreserved characters)。
code_challenge:对
code_verifier 做 SHA-256 哈希后 Base64URL
编码的结果。
code_challenge_method:S256(SHA-256)或
plain(不建议——plain 等于没做
PKCE)。
工程陷阱
1:code_challenge_method=plain
不提供任何保护——攻击者截获 code 的同时也截获了
code_challenge(code_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
把令牌绑定到特定的客户端实例,即使令牌被窃取,攻击者也无法使用。
两种主要机制:
mTLS(RFC 8705):令牌绑定到客户端的
TLS 证书。/token 请求时,OP 验证客户端证书并将
Token 绑定到该证书。后续使用 Token
访问资源时,客户端必须出示相同的 TLS 证书。要求部署 PKI
基础设施。
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/callback,https://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,让用户的账户绑定到攻击者的第三方账户)。
工程陷阱 2:state 不是
nonce。state
绑定授权请求和回调,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 的典型应用场景:
- 金融 API(FAPI
规范):用户可以授权”只允许从这个账户转出不超过 X
元”,而不是给一个”payment” scope。
- GitHub
式精细授权:授权时指定”只允许访问 repo A 和 repo
B”,而不是”所有 repos”。
RAR 在实际落地中受限于 OP 和资源服务器的支持。截至 2026
年,Auth0 和 Okta 支持
authorization_details,Keycloak 在 24.x
版本中有限支持。
六、小结:OAuth 2.1
的”底线配置”
如果你要新部署一个 OAuth
授权系统,以下配置应该作为默认起点(除非有明确理由偏离):
- 只使用 Authorization Code Grant +
PKCE,
code_challenge_method=S256。
- 所有客户端都使用
PKCE,包括机密客户端。
- Refresh Token Rotation(每次换新 token
时发新的 refresh token,旧的立即失效)。
- Sender-Constrained
Token(有条件的团队使用 DPoP,有 PKI 基础设施的用
mTLS)。
- 精确 redirect_uri 匹配。
- 对所有客户端验证
state
参数。
- SPA 使用 BFF(Backend for
Frontend)架构,不在浏览器端直接处理 token。
上一篇:企业单点登录:OIDC
与现代 SSO 下一篇:SAML
还值得学吗:企业遗留 SSO 的现实世界
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
2026-06-13 · architecture / security
OIDC 是当下企业 SSO 的事实标准,但大多数实现只用了它 20% 的规范。本文从 OIDC 核心规范出发,拆解 Authorization Code Flow + PKCE 的完整交互、ID Token 的验证规则、Discovery 与 Dynamic Registration 的互操作性机制,以及 RP-Initiated Logout 和 Session Management 的工程实现细节。
2026-06-13 · architecture / security
从 2020 年 SolarWinds 到 2024 年 Okta 支持系统泄露,身份基础设施的安全失败反复证明一件事:IAM 不是 IT 支撑系统,而是安全架构的承重墙。本文建立现代 IAM 的全景地图——从认证协议、令牌体系、权限模型到身份治理与平台选型,给出 5 个贯穿全系列的核心问题。
2026-06-15 · architecture / security
JWT 的无状态设计带来了可扩展性,但让令牌吊销变成了系统性问题——签出去的 JWT 在到期之前全是活令牌。Refresh Token Rotation、Token Introspection、基于事件的吊销通知、撤销列表——这些机制构成了身份系统的'紧急刹车',各自的成本、延迟和覆盖范围完全不同。本文拆解四种吊销机制的工程权衡。
2026-06-17 · architecture / security
前 9 篇讨论的都是'人'的身份——用户怎么登录、怎么验证。但微服务世界中,80% 的 API 调用是服务之间的。服务身份(Workload Identity)是整个 IAM 体系的另一半:mTLS 解决'传输层你是谁',SPIFFE/SPIRE 解决'在平台层你是谁且怎么证明',JWT Profile for OAuth 解决'我怎么拿到一个服务身份的 Token'。本文从这三条线拆解服务身份的工程实现。