OAuth + PKCE认证流程简析
在互联网世界里,你一定见过“使用微信登录”、“使用 GitHub 登录”的按钮。这种不需要输入第三方账号密码就能实现安全登录的技术,背后正是基于 OAuth 2.0 协议。
然而,随着移动 App 和单页应用(SPA)的普及,传统的 OAuth 2.0 暴露出了致命的安全漏洞。为了堵上这个漏洞,业界引入了一个强力外援——PKCE(Proof Key for Code Exchange)。
今天,我简单拆解一下认证流程,看懂 OAuth 2.0 + PKCE 是如何确保数据安全的。
1. 什么是 OAuth 2.0?
简单来说,OAuth 2.0 是一个“授权”协议。 它允许一个应用(客户端)在不获取用户密码的情况下,代表用户去访问另外一个服务(资源服务器)上的特定数据。
一个生活中的例子:外卖小哥与电子门禁
想象一下,你住在一家高级公寓。外卖小哥(客户端 App)要把餐送到你家门口(访问数据),但公寓大门有电子门禁(资源服务器)。
- 糟糕的做法:你把门禁密码(用户密码)直接告诉外卖小哥。这很危险,因为小哥以后随时能刷密码进你家。
- OAuth 2.0 的做法:你给外卖小哥发送一个临时二维码(Access Token,访问令牌)。小哥在门口机器上一刷,大门识别出这是你授权的、且只在 10 分钟内有效的凭证,于是放行。
2. 经典的“授权码模式 (Code Flow)”是怎样的?
在 OAuth 2.0 的众多模式中,授权码模式(Authorization Code Flow)是最安全、最常用的一种。它之所以安全,是因为它采用了“两步走”的策略:
- 第一步:拿授权码(Code) 应用先带着用户跳转到登录中心(授权服务器)。用户登录并同意授权后,服务器不会直接给应用令牌,而是先给应用一个临时的、很快就过期的授权码(Code)。
- 第二步:用码换令牌(Token) 应用在后台把这个授权码连同只有它自己知道的“客户端密钥(Client Secret)”一起发送给服务器,最终换回真正的访问令牌(Access Token)。
3. 传统授权码模式有什么缺陷?
既然“两步走”看起来天衣无缝,为什么还要引入 PKCE 呢?
因为时代变了。过去的应用大多运行在安全的服务器后端(Confidential Client),黑客拿不到服务器里的“客户端密钥”。但现在的应用大量运行在手机 App、桌面客户端或浏览器单页应用(Public Client,公共客户端)中。
对于这些“公共客户端”,传统的授权码模式存在一个致命缺陷:
授权码拦截攻击 (Authorization Code Interception Attack)
在手机系统(iOS/Android)或浏览器中,App 之间通常使用“自定义 URL Schema”(例如 myapp://callback)来接收授权服务器返回的授权码。
- 恶意应用冒充:黑客可以在用户的手机上安装一个恶意应用,并无耻地注册相同的
myapp://callback。 - 拦截授权码:当你在正版 App 里完成登录,授权服务器返回授权码时,手机系统可能会误把这个授权码传给黑客的恶意应用。
- 由于没有秘密可言:因为这是个手机 App,正版应用里根本没办法安全地隐藏
Client Secret(反编译一下就曝光了)。这意味着黑客的恶意应用同样不需要密钥,直接拿着偷来的授权码就能去换取用户的访问令牌(Access Token)!用户的数据瞬间失窃。
4. 救场英雄:什么是 PKCE?
为了解决公共客户端无法安全保存密钥、授权码容易被拦截的问题,IETF 推出了 PKCE(全称 Proof Key for Code Exchange,读作 pixie)。
PKCE 的核心思想是: 既然静态的“客户端密钥”容易泄露,那我们就干脆不要静态密钥了!改为由客户端动态生成一个一次性的、无法逆推的临时暗号。
在发起登录前,应用在本地生成两个东西:
- Code Verifier(验证码):一个随机生成的超长复杂字符串。这相当于“原始钥匙”,留在 App 内存中,绝不走网络传输。
- Code Challenge(挑战码):对上面的字符串进行 SHA-256 哈希加密,再经过 Base64URL 编码处理。这相当于“钥匙的指纹”。
5. Code + PKCE 的完整认证流程拆解
有了 PKCE 之后,整个认证流程升级为了更为严密的“密室对暗号”模式。我们根据交互规范,将其拆解为以下 5 大核心步骤:
+--------+ +---------------+
| |--(A)- 发起登录 (Challenge)---->| |
| | | |
| |<-(B)- 返回授权码 (Code)--------| 授权服务器 |
| 客户端 | | |
| App |--(C)- 换令牌 (Code+Verifier)-->| |
| | +---------------+
| |<-(D)- 校验成功,发放 Token-----| 令牌端点 |
+--------+ +---------------+
第一步:客户端本地准备
客户端(App)在内存中随机生成一个高熵字符串 code_verifier,接着计算其 SHA-256 哈希值得到 code_challenge。
第二步:重定向至授权端点(携带“指纹”)
客户端引导用户跳转到授权服务器。在发送的请求中,不仅包含传统的参数(如 client_id、redirect_uri),还带上了钥匙的指纹和加密算法:
GET /authorize?
response_type=code&
client_id=spa-client&
redirect_uri=https://app.example.com/callback&
scope=openid profile email&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256
服务器操作:授权服务器让用户输入账号密码登录。同时,服务器在后台把这个
code_challenge牢牢记在自己的小本本上。
第三步:用户同意,服务器返回授权码
用户登录并点击“同意授权”后,授权服务器通过回调地址将一次性的 code 返回给客户端。
302 Location: https://app.example.com/callback?code=SplxlOBeZQQYbYS6WxSbIA
第四步:用授权码换令牌(携带“原始钥匙”)
客户端拿到 code 后,向服务器的令牌端点(Token Endpoint)发起 POST 请求。关键点来了:这次它把一直藏在内存里的“原始钥匙” code_verifier 发了过去:
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=SplxlOBeZQQYbYS6WxSbIA&
redirect_uri=https://app.example.com/callback&
client_id=spa-client&
code_verifier=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
第五步:关键校验与令牌签发
令牌端点收到请求后,做出一个数学验证:将客户端传过来的 code_verifier 用同样的 S256 算法计算一遍,看结果是否等于第二步收到的 code_challenge。
- 如果相等:证明“现在来换令牌的 App”和“第一步发起登录的 App”是同一个应用!服务器大方发放
access_token。 - 如果不相等:拒绝请求。
6. 为什么 PKCE 能完美防御拦截攻击?
我们重新代入黑客的视角看看:
- 黑客的恶意应用利用系统漏洞,确实在第三步半路拦截到了授权码
code=SplxlOBeZQQYbYS6WxSbIA。 - 黑客欣喜若狂,试图把这个
code发给服务器换取令牌。 - 但是! 服务器的令牌端点无情地要求:“请交出你的
code_verifier(原始钥匙)。” - 黑客傻眼了,因为
code_verifier自始至终只存在于正版 App 的内存中,根本没在网络上出现过。黑客手里只有第二步公开的code_challenge(指纹),由于 SHA-256 算法是不可逆的,黑客穷尽一生也无法从指纹反推出原始钥匙。
校验失败,黑客手里的授权码直接变成废纸一张!
延伸思考:如果黑客试图拦截 code_verifier 呢?
有些同学可能会问:“既然授权码和 code_challenge 能被拦截,难道黑客就不能拦截第四步的 code_verifier 吗?”
实际上,截获 code_verifier 的难度与截获 code_challenge 相比有着天壤之别。
1. 传输通道与留痕差异 (核心防线)
code_challenge(第二步:高暴露风险):通过浏览器重定向 (GET) 传输,参数拼接在 URL 后。即使有 HTTPS,URL 仍会被记录在浏览器历史记录、反向代理(如 Nginx)访问日志和系统日志中。在移动端,恶意应用还可以注册相同的 Custom URL Schemes 直接在系统层面拦截它。code_verifier(第四步:高安全隔离):通过后台 POST 请求体发送。数据在 HTTPS 保护下是强加密的,且不会在浏览器历史或系统日志中留痕。code_verifier从生成到发送,始终保存在正版 App 的私有内存空间中。除非设备被植入木马直接读取运行内存(RAM),否则外部应用无法触及。
| 特性 | code_challenge (第二步) | code_verifier (第四步) |
|---|---|---|
| 传输位置 | URL 参数 (GET) | 请求体 (POST Body) |
| 日志留痕 | 会留在浏览器历史、服务器日志中 | 不会留痕 |
| 网络劫持难度 | 相同(均靠 HTTPS 防御) | 相同(均靠 HTTPS 防御) |
| 本地/系统劫持难度 | 极易(恶意 App 拦截、读日志) | 极难(需要获取进程内存权限) |
2. 如果 code_verifier 真的被截获了会怎样?
如果在换取 Token 时 code_verifier 真的被截获(通常意味着 HTTPS 加密已被彻底破解),OAuth 2.0 协议和认证服务器也设计了相应的安全兜底机制:
- 授权码一次性:授权码在设计上只能使用一次且有效期极短(通常 1~10 分钟)。黑客必须抢在正规客户端之前发起请求,否则授权码失效。
- 防重放与双向作废:如果认证服务器发现同一个授权码被提交了第二次,服务器会立刻让该授权码对应的所有 Token 失效(双向作废),并触发安全风控,强制中断当前会话。
因此,PKCE 的精妙之处在于默认 code_challenge 是一定会暴露的,其核心设计就是为了保护那个“只藏在内存中、只走 POST 请求”的 code_verifier。
7. 可交互动画演示 (PKCE Flow Animation)
你可以通过以下链接查看完整的 PKCE 交互流程动画演示:
总结
OAuth 2.0 的 Code + PKCE 模式,通过“前向发送哈希指纹,后向提交原始钥匙”的精妙设计,在不需要静态密钥的前提下,完美确认了客户端的身份一致性。
如今,PKCE 已经不仅仅是手机 App 和单页应用的专属。在最新的 OAuth 2.1 推荐规范中,所有类型的应用(包括拥有安全后端的服务器应用)都建议强制开启 PKCE,它已经成为了现代互联网安全认证的不二选择。
Comments (0)
No comments yet. Be the first!