前端 OAuth 登录为什么推荐使用 PKCE?它解决了什么安全问题?
简化版
PKCE 是 OAuth 授权码模式的增强机制,适合 SPA、移动端这类不能安全保存 client secret 的公开客户端。前端先生成随机 code_verifier,再生成 code_challenge 发起授权;换 token 时提交 code_verifier。即使授权码被截获,攻击者没有 verifier 也换不到 token。它主要防授权码拦截,不替代 redirect_uri 校验、state 防 CSRF 和 HTTPS。
详细版
传统授权码模式依赖 client secret,但前端代码公开,不能把 secret 放在浏览器里。PKCE 用一次性随机秘密弥补公开客户端的短板。
| 参数 | 作用 |
|---|---|
code_verifier | 前端生成并临时保存的随机字符串 |
code_challenge | verifier 的 SHA256 + base64url |
code_challenge_method | 通常为 S256 |
state | 防登录 CSRF 和流程串扰 |
生成 verifier → 计算 challenge → 跳授权页 → 拿 code → 用 code + verifier 换 token
PKCE 保护的是“授权码换 token”这一步,不能把它理解成万能登录安全方案。
完整版教学
一、为什么前端不能保存 client secret
SPA 的 JS 会下载到用户浏览器,任何人都能查看源码和网络请求。如果把 client secret 写进前端,就等于公开。
所以浏览器端属于 public client,不能依赖传统后端应用那种机密凭证。
二、授权码被截获有什么风险
OAuth 登录中,授权服务器会把 code 通过 redirect_uri 带回应用。如果 code 被恶意应用、浏览器插件、日志或中间链路截获,攻击者可能尝试用它换 token。
PKCE 要求换 token 时提交当初生成的 verifier,因此单独拿到 code 不够。
三、PKCE 流程
前端生成高熵随机字符串:
const bytes = crypto.getRandomValues(new Uint8Array(32))
然后计算 SHA-256,并做 base64url 编码作为 challenge。发起授权时带 challenge,换 token 时带原始 verifier。
四、state 仍然必须有
PKCE 不替代 state。state 用来防登录 CSRF、关联登录请求和回调,避免攻击者把自己的登录结果注入给受害者。
| 机制 | 解决问题 |
|---|---|
| PKCE | 授权码被截获后无法换 token |
| state | 防登录 CSRF 和流程串扰 |
| redirect_uri 校验 | 防 code 发到错误地址 |
| HTTPS | 防网络窃听和篡改 |
五、token 存储仍要谨慎
PKCE 解决的是换 token 的过程,不代表 token 存在浏览器就绝对安全。XSS 仍可能窃取内存或可读存储中的 token。
高安全场景可以采用 BFF 模式,由服务端保存 token,浏览器只持有 HttpOnly Cookie。
六、常见工程建议
前端接入 OAuth 时建议:
- 使用 Authorization Code + PKCE,不使用 Implicit Flow。
- redirect_uri 精确匹配,不用宽泛通配。
- state 和 nonce 使用随机值并校验。
- 全程 HTTPS。
- token 生命周期短,刷新策略清楚。
七、常见误区与追问
- 误区:PKCE 可以替代 client secret。 对公开客户端来说它增强安全,但后端 confidential client 仍应使用 secret。
- 误区:有 PKCE 就不用 state。 PKCE 防 code 截获,state 防登录 CSRF,职责不同。
- 误区:前端可以安全保存 OAuth secret。 浏览器端代码公开,secret 无法保密。
- 追问:为什么不推荐 Implicit Flow? token 直接经浏览器前端暴露,现代实践更推荐授权码 + PKCE。
- 追问:code_verifier 存在哪里? 登录流程中临时存在内存或 sessionStorage,并注意 XSS 风险。
- 追问:PKCE 能防 XSS 吗? 不能,XSS 仍可能窃取运行时数据或发起操作。
八、加强记忆
PKCE 记成“授权码的二次口令”。code 是票,verifier 是取票密码;攻击者只偷到票,没有密码也换不到 token。它要和 state、HTTPS、redirect_uri 校验一起用。