OAuth 2.0 授权码模式:四个角色与一次安全握手
授权码模式,是 OAuth 2.0 里最经典、也是最安全的一种流程。我做了这么多年安全架构,但凡涉及到第三方应用需要代表用户访问资源,首选就是它。说白了,它就像一把「临时钥匙」,你交给别人用,但随时可以收回。
这一节,我们把这套机制拆开揉碎。先搞清楚谁是谁,再看它们怎么配合。
四个角色,一个都不能少
OAuth 2.0 定义了四个核心角色。我见过不少新手搞混「授权服务器」和「资源服务器」,其实它们分工很明确。
| 角色 | 英文 | 一句话解释 | 现实类比 |
|---|---|---|---|
| 资源所有者 | Resource Owner | 数据的真正主人,通常是用户 | 酒店房客 |
| 客户端 | Client | 想要访问资源的第三方应用 | 客房清洁员 |
| 授权服务器 | Authorization Server | 负责认证用户、颁发令牌 | 前台接待处 |
| 资源服务器 | Resource Server | 存储和保护实际数据 | 客房保险柜 |
关键区别:授权服务器只管「发证」,资源服务器只管「验货」。很多系统里它们可以是同一台机器,但逻辑上必须分开。我在项目中就见过把令牌校验逻辑写在业务代码里的,后来出了问题,排查起来非常痛苦。
授权码流程:七步走完一次安全授权
这个流程看起来步骤多,但每一步都有它的道理。你想想看,如果少了任何一步,要么不安全,要么用户体验极差。
- 客户端发起请求 — 用户点击「使用登录」,客户端把用户引导到授权服务器的登录页面。
- 用户认证并授权 — 用户在授权服务器上输入账号密码,并同意客户端访问某些数据(比如头像、昵称)。
- 授权服务器返回授权码 — 授权服务器通过浏览器重定向,把授权码(Authorization Code)传给客户端。注意,这一步用的是前端通信,所以授权码是明文的。
- 客户端用授权码换令牌 — 客户端拿着授权码,在后台直接请求授权服务器,换取访问令牌(Access Token)。这一步是后端到后端,安全多了。
- 授权服务器返回访问令牌 — 授权服务器验证授权码有效后,返回访问令牌,有时还会附带刷新令牌(Refresh Token)。
- 客户端携带令牌访问资源 — 客户端在请求资源服务器的 API 时,把访问令牌放在 HTTP 头里。
- 资源服务器返回资源 — 资源服务器验证令牌有效,返回用户数据。
我的习惯:授权码一定要设置有效期,通常 5-10 分钟就够了。时间太长,被截获的风险就越大。我曾经在一个项目里看到授权码有效期设了 24 小时,当时就惊了——这等于把大门钥匙放在门口垫子下面,还放了一整天。
为什么需要授权码?直接给令牌不行吗?
这是个好问题。早期的 OAuth 2.0 隐式模式(Implicit Flow)就是直接返回令牌的。但问题在于,令牌会经过浏览器,容易被恶意脚本或中间人攻击截获。
授权码模式多了一层「授权码换令牌」的后台交互。即使授权码在传输过程中被偷了,攻击者也拿不到令牌,因为他没有客户端的密钥(Client Secret)。
避坑指南:我曾经接手过一个项目,他们把 Client Secret 硬编码在移动端 App 里。这等于把保险柜密码写在保险柜门上。正确的做法是:移动端使用 PKCE(Proof Key for Code Exchange)扩展,用动态密钥代替静态 Secret。
核心流程可视化
下面这张图,把刚才的七步流程串起来了。我建议你多看几遍,直到能在脑子里把每一步的通信方向、数据内容都过一遍。
令牌与授权码的区别
很多人分不清授权码和访问令牌。我打个比方:授权码是「取票凭证」,你拿着它去柜台换演出门票;访问令牌就是那张「门票」,入场时给检票员看就行。
| 属性 | 授权码 (Authorization Code) | 访问令牌 (Access Token) |
|---|---|---|
| 用途 | 换取访问令牌 | 直接访问资源 |
| 有效期 | 极短(通常几分钟) | 较短(通常几小时) |
| 传输方式 | 前端重定向(URL 参数) | 后端请求(HTTP Header) |
| 是否需要 Client Secret | 不需要(换令牌时需要) | 不需要 |
| 泄露风险 | 中等(有效期短,风险可控) | 高(直接可用) |
一个简单的代码示例
下面是一个简化版的授权码流程实现。注意,这只是演示逻辑,生产环境请使用成熟的 OAuth 库。
// 1. 客户端引导用户到授权服务器
// 用户点击后,浏览器跳转到:
// https://auth.example.com/authorize?
// response_type=code&
// client_id=YOUR_CLIENT_ID&
// redirect_uri=https://client.example.com/callback&
// scope=profile&
// state=random_state_string
// 2. 用户认证并授权后,授权服务器重定向回客户端
// GET https://client.example.com/callback?code=AUTH_CODE&state=random_state_string
// 3. 客户端在后台用授权码换令牌
POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE&
redirect_uri=https://client.example.com/callback&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET
// 4. 授权服务器返回访问令牌
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "def50200df...",
"scope": "profile"
}
// 5. 客户端使用访问令牌请求资源
GET https://api.example.com/user/profile
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
我的建议:一定要校验 state 参数。它可以防止跨站请求伪造(CSRF)攻击。我见过一个案例,就是因为没校验 state,攻击者伪造了回调请求,拿到了用户的授权码。虽然还有 Client Secret 保护,但多一层防护总是好的。
常见误区与最佳实践
- 误区一:把授权码直接当令牌用。授权码只能换一次令牌,换完就失效。
- 误区二:在客户端存储 Client Secret。公开客户端(如浏览器、移动 App)无法安全存储密钥,必须使用 PKCE。
- 误区三:忽略令牌过期时间。访问令牌过期后,应该用刷新令牌获取新的,而不是让用户重新登录。
- 最佳实践:始终使用 HTTPS。所有 OAuth 通信都应该加密,否则令牌和授权码都可能被中间人截获。
我曾经踩过的坑:有一次,我在一个微服务架构里,把授权服务器和资源服务器完全分开部署,但忘了同步时钟。结果资源服务器校验令牌时,因为时间偏差,所有令牌都显示「已过期」。从那以后,我养成了一个习惯:所有涉及令牌校验的服务,都用 NTP 同步时间,并且留出 5 分钟的时钟偏差容忍窗口。
授权码模式,说白了就是「让专业的人做专业的事」。授权服务器负责认证和发证,资源服务器负责保护数据,客户端只负责展示。各司其职,才能构建出既安全又灵活的系统。