🔐 为什么 PKCE 能挡住 OAuth 授权码劫持

很多人第一次接触 OAuth 2.0,会以为拿到授权码 authorization code 就已经很安全了,因为真正换 access token 还要再走一步。问题就在这里:如果攻击者能在这“一步之隔”里偷到授权码,而服务端又没有额外绑定这个授权码到底属于谁,那他就能自己拿着这串 code 去换 token,等于把你的登录结果截胡了。这类问题最常见在移动端、桌面端、单页应用,或者 redirect_uri 校验做得很松的时候。

PKCE 的核心不是“加密授权码”,而是给这次登录流程加一条只有发起者自己知道的暗号。客户端先生成一个随机字符串 code_verifier,再把它变成 code_challenge 发给授权服务器。等用户完成登录、服务器把 authorization code 发回来之后,客户端还必须拿出原始的 code_verifier 才能换 token。这样一来,攻击者就算在中途截获了 authorization code,没有最初那串 verifier,也换不到 token。你可以把它理解成:授权码只是取件号,真正能取走包裹的,是取件号加取件密码。

这事为什么会发生?因为 OAuth 最早更多是给“有后端、能保管 client_secret 的 Web 应用”设计的,后来移动 App、SPA、桌面工具也大量使用同一套协议,但这些环境根本藏不住 secret。开发者如果还沿用“有 code 就能换 token”的思路,就会把本来应该在后端完成的信任关系,错误地暴露给不可信的客户端环境。PKCE 本质上是在承认现实:前端和原生客户端不值得被默认信任,所以必须给授权码再加一次绑定。

现实里怎么防,关键不是“支持 PKCE”四个字,而是别把它做成摆设。第一,公开客户端 public client 一律强制 PKCE,别当成可选项。第二,code_verifier 必须足够随机,不能自己手搓一个短字符串糊弄过去。第三,优先使用 S256,不要退回 plain。第四,redirect_uri 要精确匹配,不能只按前缀比较,不然攻击者能把授权码送到他自己的地址。第五,授权码必须短时有效且一次性使用,用过立刻作废。第六,客户端别把 token 存在随便一个能被脚本读到的地方,不然前面防住了,后面自己又漏了。

🧪 场景分析题:一家网站的移动 App 已经接入 OAuth 登录。安全测试时发现,App 使用 Authorization Code Flow,但 token 端点只校验 client_id 和 authorization code,没有要求 code_verifier。攻击者能在受害者手机上通过恶意应用劫持回调 URI,拿到这次登录返回的 code。问:攻击者此时能不能换到 access token,真正的问题出在哪?


💡 核心记忆:PKCE 防的不是“授权码泄露”本身,而是“别人捡到授权码后也能直接换 token”这件事。

#Security