授权码模式(Authorization Code)详解

授权码模式,说白了就是OAuth2里最经典、最安全的那一套流程。我做了这么多年安全架构,90%以上的企业级应用都在用这个模式。为什么?因为它把用户凭证和访问令牌彻底分开了,安全性拉满。

咱们一步步拆解,看看它到底是怎么玩的。

交互流程:四步走

整个流程其实就四个步骤,我习惯叫它「四步握手」。

  1. 用户点击登录:用户在浏览器里点「用微信登录」或「用Google登录」
  2. 拿到授权码:授权服务器返回一个临时的授权码
  3. 交换令牌:后端拿着授权码去换真正的访问令牌
  4. 访问资源:用令牌去拿用户信息

你想想看,这里最关键的是第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,或者直接用隐式模式。不过那是另一个话题了,咱们后面再聊。