本文目录导读:
我们来详细解释一下 PKCE (Proof Key for Code Exchange,代码交换证明密钥) 加强的授权码流程。
PKCE 是 OAuth 2.0 授权码流程的一个安全增强版本,主要目的是防止授权码被拦截和窃取(即“授权码拦截攻击”)。
为什么需要 PKCE?—— 解决了什么问题?
在传统的 OAuth 2.0 授权码流程中,客户端(比如手机 App 或单页应用)需要两个关键信息:
- 授权码 (Authorization Code): 临时且短暂有效。
- 客户端密钥 (Client Secret): 用于在后台交换授权码为访问令牌 (Access Token)。
问题在于:
对于原生移动应用(iOS/Android App)或单页应用(SPA,如 React/Vue 构建的网站),client_secret 很容易被反编译或从浏览器调试工具中获取,无法安全地保密,如果攻击者截获了授权码,他就可以利用自己获取到的 client_secret 来换取访问令牌,从而窃取用户数据。
PKCE 的解决方案:
PKCE 引入了一个动态生成的“代码验证器 (Code Verifier)”和一个由其派生出的“代码挑战 (Code Challenge)”,彻底消除了静态 client_secret 的依赖,即使授权码被拦截,攻击者也因为不知道代码验证器而无法换取令牌。
PKCE 的核心机制:代码验证器与代码挑战
PKCE 流程的核心是两个动态生成的字符串:
-
代码验证器 (Code Verifier):
- 一个高熵(随机性强)的加密随机字符串。
- 由客户端在授权请求开始前自行生成。
- 长度至少 43 个字符,最多 128 个字符(RFC 7636 标准)。
-
代码挑战 (Code Challenge):
- 由代码验证器通过一个单向哈希函数(通常是
S256,即 SHA-256 算法)计算得出。 - 也可以不使用哈希,直接将“代码验证器”作为“代码挑战”(
plain方法,但安全性较低,不推荐)。 - 计算公式:
base64url(SHA256(code_verifier))。
- 由代码验证器通过一个单向哈希函数(通常是
PKCE 加强版授权码流程步骤

(图片来源:Auth0/Ping Identity 等,用于示意流程)
假设用户想要通过一个第三方 App (客户端) 登录你的服务 (授权服务器)。
-
客户端生成密钥对:
- 客户端(App/网页)生成一个随机的
code_verifier。 - 客户端使用
S256算法计算code_challenge = SHA256(code_verifier)。
- 客户端(App/网页)生成一个随机的
-
发起授权请求 (带挑战):
- 客户端引导用户浏览器/应用跳转到授权服务器的登录界面。
- 关键变化: 请求参数中不仅包含
response_type=code和redirect_uri,还额外包含了:code_challenge:上一步计算出的挑战值。code_challenge_method:使用的哈希方法(通常是S256)。
-
用户授权:
用户在授权服务器上登录并授权客户端访问其数据。
-
授权服务器返回授权码:
- 授权服务器记录下当前会话对应的
code_challenge和code_challenge_method。 - 然后通过重定向将
authorization_code发送回客户端的redirect_uri。
- 授权服务器记录下当前会话对应的
-
客户端用授权码换取令牌 (带验证器):
- 客户端收到授权码后,再次向授权服务器的令牌端点 (
/tokenendpoint) 发起请求。 - 关键变化: 请求中除了
grant_type=authorization_code和code之外,必须包含:code_verifier:之前生成的原始随机字符串。
- 客户端收到授权码后,再次向授权服务器的令牌端点 (
-
授权服务器验证挑战:
- 授权服务器收到
code_verifier和authorization_code。 - 它根据之前记录的
code_challenge_method,对收到的code_verifier使用同样的哈希算法(S256)进行计算。 - 比较:
hash(code_verifier)等于 之前存储的code_challenge,则验证通过。 - 只有验证通过,授权服务器才会返回
access_token。
- 授权服务器收到
为什么这能防止攻击?
现在假设一个攻击者截获了步骤 4 中的 authorization_code,他立即尝试去令牌端点换取 access_token。
- 攻击者需要提供
code_verifier。 - 但他无法知道客户端在步骤 1 中生成的随机
code_verifier(因为它从未在网络上传输过,客户端只发送了它的哈希值code_challenge)。 - 攻击者也无法从
code_challenge反推出code_verifier,因为哈希函数是单向的。 - 攻击者的令牌请求会失败,因为授权服务器计算出的哈希值不匹配。
即使授权码被泄露,它也无法被攻击者使用,因为缺少了只有原始客户端才知道的“秘密”——代码验证器。
关键区别总结表
| 特性 | 传统授权码流程 (No PKCE) | PKCE 加强授权码流程 |
|---|---|---|
| 客户端密钥 | 需要,且必须保密 (Client Secret) | 不需要 Client Secret,本身不保密也无妨 |
| 核心秘密 | 静态、长期、易泄露的 Client Secret | 动态、一次性、随机生成的 Code Verifier |
| 安全风险 | 授权码拦截后可立即换取令牌 | 授权码拦截后因缺少 Code Verifier 而无法换取令牌 |
| 主要适用场景 | 有后端服务器、能安全保存密钥的 Web 应用 (如Java, .NET, Python后端) | 所有公共客户端:原生移动App、单页应用 (SPA)、桌面应用、IoT设备 |
| 复杂度 | 较低 | 稍高(需要生成随机字符串和哈希计算) |
| 标准支持 | OAuth 2.0 | OAuth 2.0 + PKCE (RFC 7636) |
PKCE 是推荐标准
- OAuth 2.0 Security Best Current Practice (BCP): IETF 最新发布的安全最佳实践文档中,强烈建议所有类型的 OAuth 2.0 客户端都使用 PKCE,即使是有能力安全存储
client_secret的机密客户端,这被称为“防御性纵深”策略。 - App 审核要求: 许多平台(如 Google、Apple、微软的认证流程)强制要求使用 PKCE,特别是对于移动应用和 SPA 应用。
实践建议
- 如果你是开发者: 在进行 OAuth 2.0 集成时,无论你的应用类型(Web、App、桌面),默认使用 PKCE,大多数现代的 OAuth 2.0 库(如 Spring Security、OIDC-Client、AppAuth、MSAL 等)都已经内置或强烈推荐使用 PKCE。
- 注意
code_challenge_method: 永远使用S256(SHA-256 哈希),不要使用plain(明文),plain几乎没有任何安全增益。 code_verifier的生成: 使用密码学安全的随机数生成器(如crypto.randomUUID()或secrets.token_urlsafe())生成一个足够长的随机字符串(43-128 字符)。
总结一句话
PKCE 通过让客户端动态生成一个只有自己知道的临时密钥(Code Verifier),并使用其哈希值(Challenge)进行“一对一、一次性”的绑定,从而彻底消除了传统 OAuth 2.0 授权码流程中最薄弱的环节——静态的、易泄露的 Client Secret,将授权码拦截攻击的风险降到最低。 现在它是所有 OAuth 2.0 实现的标配安全要求。