OAuth2.0协议:授权码模式、客户端模式、刷新令牌与安全实践
OAuth2.0 这东西,说白了就是一个「令牌分发协议」。它不关心你是谁,只关心你有没有令牌。我在审计 SDK 的越权漏洞时,发现 90% 的问题都出在对这个协议的理解不深,或者实现上有偏差。今天我们就来把它的几种核心流程掰开了讲清楚。
授权码模式:最安全的首选
这是 OAuth2.0 里最安全、也是最常用的模式。它专门为有后端服务器的应用设计。
核心流程(简化版)
- 用户点击“第三方登录”,浏览器跳转到授权服务器。
- 用户同意授权,授权服务器返回一个临时的「授权码」到你的后端回调地址。
- 你的后端拿着这个授权码,去授权服务器换「访问令牌」。
- 之后用访问令牌请求资源。
我的习惯
我在审计 SDK 时,特别关注第 2 步和第 3 步之间。授权码必须是一次性的,而且要绑定客户端的 client_id 和回调地址。为什么?因为如果在传输中授权码被截获,攻击者用它也换不到令牌——因为他的回调地址不匹配。这就是授权码模式的安全精髓。
我在项目中遇到过一种情况,某个 SDK 的回调地址参数没有做严格校验,导致攻击者可以通过修改 redirect_uri 来劫持授权码。嗯,这其实是一个经典的「回调地址注入」漏洞。
客户端模式:适合内部服务
这种模式没有用户参与,是服务与服务之间的授权。很多微服务架构中会用这个。
- 适用场景:服务器之间的通信、定时任务、后台批处理
- 特点:直接使用 client_id 和 client_secret 换取令牌,没有用户交互
- 安全风险:client_secret 一旦泄露,攻击者就能伪造请求
避坑指南
我曾经在一个项目里看到,开发人员把客户端模式的 client_secret 硬编码在移动端 App 里。你想想看,App 是可以被反编译的。这种模式下,secret 一旦被提取,整个服务接口就相当于裸奔了。
我建议:客户端模式的 secret 只能放在后端配置中心或密钥管理服务里,不能出现在前端或客户端代码中。
刷新令牌:保持长尾会话
访问令牌通常有过期时间,短则 15 分钟,长则 1 小时。刷新令牌就是用来续命的。
| 对比项 | 访问令牌 | 刷新令牌 |
|---|---|---|
| 用途 | 访问资源 | 换新的访问令牌 |
| 有效期 | 短(分钟/小时级) | 长(天/周级) |
| 泄露后风险 | 高(直接可用) | 中(需要配合 client_secret) |
| 是否可撤销 | 通常不可 | 可以(服务端主动使失效) |
我的实践
我建议在实现刷新逻辑时,一定要做「刷新令牌轮换」(Refresh Rotation)。也就是说,每次刷新后,旧的刷新令牌立即失效。我曾经审计过一个 SDK,它允许同一个刷新令牌反复使用——这意味着一旦泄露,攻击者可以长时间接管用户会话。
安全实践:那些踩过的坑
我在安全实践中做过一个统计,OAuth2.0 实现中的常见问题可以分为以下几类:
- CSRF 攻击:授权码模式没有使用 state 参数,导致攻击者可以伪造授权回调。
- 令牌泄露:访问令牌通过 URL 参数传递(你该用 POST body 或者 Header)。
- 重定向 URI 校验不严:允许通配符或子路径匹配,这很危险。
- 令牌生命周期管理缺失:没有黑名单机制,泄露的令牌无法立即作废。
- 权限范围过大:申请了 scope,但没有在服务端做最小权限校验。
我常说一句话:授权码是临时凭证,访问令牌是通行证,刷新令牌是 VIP 续期卡。你对待它们的安全等级应该逐级升高。
OAuth2.0 四种核心流程关系图
我个人一直坚持三条铁律
- 永远不要在客户端存储刷新令牌。一旦 App 被反编译,长期令牌暴露,用户会话就永久可被劫持。
- 授权码模式必须校验 state 参数。这能直接防御 CSRF 类型的越权攻击。我在审计时,第一件事就是看 state 有没有被正确传递和校验。
- 客户端模式的 secret 只能放在服务端环境变量或密钥管理服务中。不要问能不能放数据库——那也不行,除非做了加密且密钥分开管理。
其实 OAuth2.0 本身是一个中立的协议,安全性高度依赖实现。我在项目中遇到过最典型的错误,就是把客户端模式用在了需要用户授权的场景里,结果所有操作都变成了同一个身份——这就是越权漏洞的温床。