授权码模式(Authorization Code)详解
授权码模式,说白了就是OAuth2里最经典、最安全的那一套流程。我做了这么多年安全架构,90%以上的企业级应用都在用这个模式。为什么?因为它把用户凭证和访问令牌彻底分开了,安全性拉满。
咱们一步步拆解,看看它到底是怎么玩的。
交互流程:四步走
整个流程其实就四个步骤,我习惯叫它「四步握手」。
- 用户点击登录:用户在浏览器里点「用微信登录」或「用Google登录」
- 拿到授权码:授权服务器返回一个临时的授权码
- 交换令牌:后端拿着授权码去换真正的访问令牌
- 访问资源:用令牌去拿用户信息
你想想看,这里最关键的是第2步和第3步。授权码只在浏览器里短暂出现,真正的令牌交换是在后端完成的。这就避免了令牌暴露在浏览器里。
核心要点:授权码是一次性的,有效期通常只有几分钟。我见过有人把授权码有效期设成1小时,结果被重放攻击了——嗯,那是个惨痛的教训。
授权码请求:第一步怎么走
用户点击登录后,浏览器会跳转到授权服务器的登录页面。这个跳转URL长这样:
GET /authorize?
response_type=code
&client_id=your-client-id
&redirect_uri=https://yourapp.com/callback
&scope=openid profile
&state=xyz123
这里有几个参数我得重点说说:
- response_type=code:告诉服务器我要授权码模式
- client_id:你的应用在授权服务器注册时拿到的ID
- redirect_uri:用户授权后跳回哪里
- state:防CSRF攻击的随机字符串,这个很重要
我的经验:state参数很多人会忽略。我曾经在一个项目里发现,没有state参数,攻击者可以伪造授权回调。从那以后,我要求所有项目必须生成随机state并校验。
用户登录并授权后,浏览器会重定向到你的回调地址,带上授权码:
GET https://yourapp.com/callback?code=AUTH_CODE_123&state=xyz123
令牌交换:后端干的活
拿到授权码后,你的后端服务器要发起一个POST请求,去换令牌。这个请求绝对不能从浏览器发,必须从服务器发。
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTH_CODE_123
&redirect_uri=https://yourapp.com/callback
&client_id=your-client-id
&client_secret=your-client-secret
授权服务器验证通过后,会返回一个JSON:
{
"access_token": "ACCESS_TOKEN_456",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "REFRESH_TOKEN_789",
"scope": "openid profile"
}
这里有个细节:client_secret绝对不能泄露。我见过有人把client_secret写在前端代码里,那基本等于把家门钥匙挂在门口。
| 参数 | 说明 | 安全要求 |
|---|---|---|
| grant_type | 固定为authorization_code | 无 |
| code | 上一步拿到的授权码 | 一次性使用,用完即废 |
| redirect_uri | 必须和第一步的完全一致 | 防止重定向劫持 |
| client_secret | 应用密钥 | 仅后端存储,绝不外泄 |
安全考量:那些容易踩的坑
授权码模式虽然安全,但用不好照样出问题。我总结几个常见的安全要点:
1. 授权码重用攻击
授权码只能用一次。服务器收到令牌交换请求后,必须立即标记该授权码为已使用。如果有人试图用同一个授权码再换一次,直接拒绝。
警告:我曾经遇到过一个案例,授权码没有做一次性校验,攻击者截获了授权码后反复使用,直到过期。修复方案很简单:在数据库里加个used字段,交换成功后立即更新。
2. 重定向URI校验
授权服务器必须严格校验redirect_uri。不能只校验域名前缀,要校验完整路径。比如你的回调地址是https://yourapp.com/callback,那攻击者用https://yourapp.com/evil就不应该通过。
3. CSRF攻击防护
state参数就是干这个的。你的应用在发起授权请求时生成一个随机state,回调时校验state是否一致。不一致就直接拒绝。
// 生成state
String state = UUID.randomUUID().toString();
session.setAttribute("oauth_state", state);
// 回调时校验
String receivedState = request.getParameter("state");
String savedState = (String) session.getAttribute("oauth_state");
if (!savedState.equals(receivedState)) {
throw new SecurityException("CSRF攻击检测");
}
4. 授权码泄露风险
授权码会出现在浏览器的地址栏里。如果有人能偷看你的屏幕,或者浏览器历史记录被窃取,授权码就可能泄露。所以授权码的有效期一定要短,我一般设成5分钟。
小技巧:可以在授权码里嵌入时间戳和签名,服务端解码后校验签名和时效性。这样即使数据库查询慢一点,也能快速判断授权码是否有效。
实战中的最佳实践
说了这么多理论,咱们看看实际项目中怎么用。我个人习惯用Spring Security的OAuth2客户端,它把授权码模式封装得很好:
spring:
security:
oauth2:
client:
registration:
my-app:
client-id: your-client-id
client-secret: your-client-secret
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
scope: openid, profile
provider:
my-app:
authorization-uri: https://auth-server.com/authorize
token-uri: https://auth-server.com/token
配置完这些,Spring Security会自动处理授权码的获取和令牌交换。你只需要关注业务逻辑就好。
但记住,自动化的东西也要理解背后的原理。我见过有人配置完就跑,出了问题完全不知道从哪排查。授权码模式的核心就是那四步握手,每一步都有它的安全考量。
最后说一句:授权码模式不是万能的。如果你的应用是纯前端SPA,没有后端服务器,那授权码模式就不太合适。这时候可以考虑授权码模式+PKCE,或者直接用隐式模式。不过那是另一个话题了,咱们后面再聊。