2. OAuth2授权码模式(Authorization Code)

授权码模式是OAuth2协议中最完整、最安全的授权流程,广泛应用于服务端渲染的Web应用。该模式通过引入一个临时的“授权码”作为中间凭证,确保访问令牌(Access Token)不会暴露在用户代理(浏览器)端,从而有效降低令牌泄露风险。

2.1 授权码流程详解

授权码模式涉及四个核心角色:资源所有者(用户)、用户代理(浏览器)、客户端(Web应用)、授权服务器。整个流程分为两个阶段:授权码获取阶段和令牌交换阶段。

核心安全原则:授权码是单次使用、短期有效的临时凭证,即使被截获,攻击者也无法在无客户端凭证(Client Secret)的情况下完成令牌交换。

完整流程步骤

  1. 用户发起登录:用户在客户端点击“使用OAuth登录”,客户端构造授权请求URL,重定向用户至授权服务器。
  2. 用户认证与授权:用户在授权服务器上输入凭证并确认授权范围(Scope)。
  3. 返回授权码:授权服务器通过重定向将授权码(Authorization Code)发送回客户端的回调地址(Redirect URI)。
  4. 后端令牌交换:客户端服务器使用授权码 + 客户端凭证(Client ID + Client Secret),通过后端通道向授权服务器请求访问令牌。
  5. 返回访问令牌:授权服务器验证授权码和客户端凭证后,返回Access Token(可选返回Refresh Token)。
  6. 访问资源:客户端使用Access Token请求资源服务器上的受保护资源。
授权码模式完整流程 授权码模式完整流程 用户(浏览器) 客户端(Web) 授权服务器 资源服务器 ① 发起登录 ② 授权请求 ③ 用户认证与授权 ④ 返回授权码 ⑤ 令牌交换 ⑥ 返回Access Token ⑦ 请求资源 ⑧ 返回资源 关键安全设计 • 授权码通过前端通道(浏览器重定向)传递,Access Token通过后端通道(服务器直连)传递。 • 授权码有效期极短(通常60秒),且只能使用一次。 • 令牌交换时必须验证Client Secret,防止授权码被第三方客户端滥用。 • Redirect URI必须与注册时完全一致,防止开放重定向攻击。

2.2 授权码请求与令牌交换

2.2.1 授权码请求(Authorization Request)

客户端构造授权请求URL,将用户重定向到授权服务器的 /authorize 端点。请求参数通过URL查询字符串传递。

参数 是否必需 说明
response_type 是 固定值为 code,表示使用授权码模式
client_id 是 客户端在授权服务器注册时获得的唯一标识
redirect_uri 是 授权服务器回调地址,必须与注册时一致
scope 否 请求的权限范围,如 openid profile email
state 强烈推荐 防CSRF攻击的随机字符串,将在2.3节详述
最佳实践:始终传递 state 参数,即使授权服务器不强制要求。这是防御CSRF攻击的第一道防线。

授权请求示例:

GET /authorize?
  response_type=code
  &client_id=web_app_001
  &redirect_uri=https://example.com/callback
  &scope=openid%20profile
  &state=xyzABC123
HTTP/1.1
Host: auth.example.com

2.2.2 令牌交换(Token Exchange)

客户端收到授权码后,通过后端通道(服务器到服务器)向授权服务器的 /token 端点发起POST请求,交换Access Token。

参数 是否必需 说明
grant_type 是 固定值为 authorization_code
code 是 上一步获取的授权码
redirect_uri 是 必须与授权请求中的redirect_uri一致
client_id 是 客户端标识
client_secret 是 客户端密钥,仅在后端通道传输

令牌交换请求示例:

POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=abc123xyz
&redirect_uri=https://example.com/callback
&client_id=web_app_001
&client_secret=secret_key_123

令牌响应示例:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "def456ghi",
  "scope": "openid profile"
}

2.3 CSRF攻击防护与state参数

2.3.1 CSRF攻击场景

在授权码模式中,CSRF(跨站请求伪造)攻击的核心风险是:攻击者诱导用户点击恶意链接,将攻击者自己的授权码注入到受害者的客户端会话中,从而绑定攻击者的账户到受害者的客户端。

攻击后果:攻击者可以将受害者的客户端账户与攻击者的第三方账户绑定,后续攻击者通过自己的第三方账户访问受害者在客户端上的数据。

2.3.2 state参数防护机制

state 参数是一个由客户端生成的不可预测的随机字符串,在授权请求时发送给授权服务器,授权服务器在回调时原样返回。客户端通过验证回调中的 state 值与发起请求时存储的值是否一致,来确认回调请求的合法性。

CSRF攻击与state参数防护 CSRF攻击与state参数防护 攻击场景(无state防护) 攻击者 ① 构造恶意链接 受害者 ② 点击链接 客户端 ③ 注入攻击者授权码 结果 ④ 账户绑定被篡改 防护场景(使用state参数) 客户端 ① 生成随机state并存储 授权请求 ② 携带state参数 回调验证 ③ 验证state一致性 结果 ④ 拒绝不匹配的请求 state参数实现要点 1. 生成:使用密码学安全的随机数生成器(如 crypto.randomBytes),长度至少16字节。 2. 存储:将state与用户会话绑定,存储在服务端Session或加密Cookie中。 3. 验证:回调处理时,比较URL中的state与会话中的state,不一致则拒绝请求。 4. 时效:state应在授权请求发起后一段时间内有效,建议设置5分钟过期。

2.3.3 state参数实现示例

服务端生成与存储(Node.js示例):

const crypto = require('crypto');

// 生成随机state
function generateState() {
  return crypto.randomBytes(16).toString('hex');
}

// 存储state到会话
app.get('/auth/login', (req, res) => {
  const state = generateState();
  req.session.oauthState = state;
  
  const authUrl = `https://auth.example.com/authorize?
    response_type=code
    &client_id=web_app_001
    &redirect_uri=https://example.com/callback
    &state=${state}`;
    
  res.redirect(authUrl);
});

回调验证state:

app.get('/callback', (req, res) => {
  const { code, state } = req.query;
  
  // 验证state
  if (!state || state !== req.session.oauthState) {
    // 清除会话中的state
    delete req.session.oauthState;
    return res.status(403).send('CSRF攻击检测:state参数不匹配');
  }
  
  // state验证通过,清除已使用的state
  delete req.session.oauthState;
  
  // 继续令牌交换流程
  exchangeCodeForToken(code);
});
合规审计要点:在PCI DSS、SOC2等合规框架下,state参数的实现必须满足以下要求:
• 使用FIPS 140-2验证的随机数生成器
• state值在验证后立即失效(单次使用)
• 会话超时后state自动失效
• 日志记录state验证失败事件,用于安全监控

2.3.4 常见CSRF攻击绕过与防御

攻击方式 描述 防御措施
state参数固定 攻击者使用固定的state值进行重放 每次授权请求生成新的随机state,验证后立即销毁
state参数泄露 state通过Referer头或日志泄露 使用HTTPS传输,不在日志中记录完整state
会话固定攻击 攻击者预先设置受害者的会话state state绑定到用户会话ID,拒绝未预期的state值
开放重定向 攻击者篡改redirect_uri指向恶意站点 严格验证redirect_uri与注册时完全一致