2. OAuth2授权码模式(Authorization Code)
授权码模式是OAuth2协议中最完整、最安全的授权流程,广泛应用于服务端渲染的Web应用。该模式通过引入一个临时的“授权码”作为中间凭证,确保访问令牌(Access Token)不会暴露在用户代理(浏览器)端,从而有效降低令牌泄露风险。
2.1 授权码流程详解
授权码模式涉及四个核心角色:资源所有者(用户)、用户代理(浏览器)、客户端(Web应用)、授权服务器。整个流程分为两个阶段:授权码获取阶段和令牌交换阶段。
核心安全原则:授权码是单次使用、短期有效的临时凭证,即使被截获,攻击者也无法在无客户端凭证(Client Secret)的情况下完成令牌交换。
完整流程步骤
- 用户发起登录:用户在客户端点击“使用OAuth登录”,客户端构造授权请求URL,重定向用户至授权服务器。
- 用户认证与授权:用户在授权服务器上输入凭证并确认授权范围(Scope)。
- 返回授权码:授权服务器通过重定向将授权码(Authorization Code)发送回客户端的回调地址(Redirect URI)。
- 后端令牌交换:客户端服务器使用授权码 + 客户端凭证(Client ID + Client Secret),通过后端通道向授权服务器请求访问令牌。
- 返回访问令牌:授权服务器验证授权码和客户端凭证后,返回Access Token(可选返回Refresh Token)。
- 访问资源:客户端使用Access Token请求资源服务器上的受保护资源。
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 值与发起请求时存储的值是否一致,来确认回调请求的合法性。
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验证失败事件,用于安全监控
• 使用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与注册时完全一致 |