OAuth 安全最佳实践 #
概述 #
OAuth 2.0 的安全性依赖于正确的实现。本文档总结了 OAuth 安全的最佳实践,帮助你构建安全的授权系统。
text
┌─────────────────────────────────────────────────────────────┐
│ OAuth 安全核心原则 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 最小权限原则 │
│ - 只请求必要的 scope │
│ - 令牌权限最小化 │
│ │
│ 2. 深度防御 │
│ - 多层安全措施 │
│ - 不依赖单一防护 │
│ │
│ 3. 安全默认 │
│ - 默认配置应该是最安全的 │
│ - 需要明确选择降低安全性 │
│ │
│ 4. 纵深验证 │
│ - 验证所有输入 │
│ - 不信任任何外部数据 │
│ │
└─────────────────────────────────────────────────────────────┘
常见攻击与防护 #
1. CSRF 攻击 #
text
┌─────────────────────────────────────────────────────────────┐
│ CSRF 攻击 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 攻击场景: │
│ 1. 用户已登录授权服务器 │
│ 2. 攻击者诱导用户访问恶意页面 │
│ 3. 恶意页面发起授权请求 │
│ 4. 用户在不知情的情况下授权 │
│ │
│ 防护措施: │
│ ✅ 使用 state 参数 │
│ ✅ 验证 state 参数 │
│ ✅ state 使用加密随机值 │
│ ✅ state 绑定用户会话 │
│ │
└─────────────────────────────────────────────────────────────┘
防护实现 #
javascript
function generateState() {
return crypto.randomBytes(32).toString('hex');
}
function login(req, res) {
const state = generateState();
req.session.oauthState = state;
const authUrl = `${authEndpoint}?` +
`response_type=code&` +
`client_id=${clientId}&` +
`redirect_uri=${redirectUri}&` +
`state=${state}`;
res.redirect(authUrl);
}
function callback(req, res) {
const { code, state } = req.query;
if (!state || state !== req.session.oauthState) {
return res.status(400).send('Invalid state');
}
delete req.session.oauthState;
}
2. 授权码劫持 #
text
┌─────────────────────────────────────────────────────────────┐
│ 授权码劫持攻击 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 攻击场景: │
│ 1. 攻击者注册恶意 URL Scheme │
│ 2. 用户发起授权请求 │
│ 3. 授权码被恶意应用截获 │
│ 4. 攻击者使用授权码获取令牌 │
│ │
│ 防护措施: │
│ ✅ 使用 PKCE │
│ ✅ 严格验证 redirect_uri │
│ ✅ 授权码短期有效 │
│ ✅ 授权码一次性使用 │
│ │
└─────────────────────────────────────────────────────────────┘
3. 开放重定向 #
text
┌─────────────────────────────────────────────────────────────┐
│ 开放重定向攻击 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 攻击场景: │
│ 1. 攻击者构造恶意 redirect_uri │
│ 2. 用户授权后重定向到攻击者控制的服务器 │
│ 3. 授权码或令牌泄露 │
│ │
│ 防护措施: │
│ ✅ redirect_uri 严格白名单验证 │
│ ✅ 完全匹配(协议、主机、端口、路径) │
│ ✅ 不允许通配符 │
│ ✅ 不允许动态 redirect_uri │
│ │
└─────────────────────────────────────────────────────────────┘
redirect_uri 验证 #
javascript
const ALLOWED_REDIRECT_URIS = [
'https://app.example.com/callback',
'https://app.example.com/auth/callback'
];
function validateRedirectUri(redirectUri) {
if (!redirectUri) {
return false;
}
try {
const url = new URL(redirectUri);
if (url.protocol !== 'https:') {
return false;
}
return ALLOWED_REDIRECT_URIS.includes(redirectUri);
} catch {
return false;
}
}
4. 令牌泄露 #
text
┌─────────────────────────────────────────────────────────────┐
│ 令牌泄露风险 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 泄露途径: │
│ - 浏览器历史记录 │
│ - Referer 头 │
│ - 服务器日志 │
│ - XSS 攻击 │
│ - 网络监听 │
│ │
│ 防护措施: │
│ ✅ 使用短期令牌 │
│ ✅ 令牌不存储在 URL 中 │
│ ✅ 使用 HTTPS │
│ ✅ 安全存储令牌 │
│ ✅ 实现令牌撤销机制 │
│ │
└─────────────────────────────────────────────────────────────┘
5. 混淆攻击 #
text
┌─────────────────────────────────────────────────────────────┐
│ 混淆攻击 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 攻击场景: │
│ 1. 攻击者获取 ID Token │
│ 2. 将 ID Token 当作 Access Token 使用 │
│ 3. 或者将 Access Token 当作 ID Token │
│ │
│ 防护措施: │
│ ✅ 严格区分令牌类型 │
│ ✅ 验证令牌用途 │
│ ✅ 使用不同的密钥签名 │
│ ✅ 验证 aud 声明 │
│ │
└─────────────────────────────────────────────────────────────┘
令牌安全 #
令牌存储 #
text
┌─────────────────────────────────────────────────────────────┐
│ 令牌存储位置 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 浏览器环境: │
│ ┌─────────────────┬─────────────────────────────────────┐ │
│ │ 存储位置 │ 安全性评估 │ │
│ ├─────────────────┼─────────────────────────────────────┤ │
│ │ localStorage │ ❌ 不推荐,易受 XSS 攻击 │ │
│ │ sessionStorage │ ⚠️ 一般,关闭浏览器后清除 │ │
│ │ HttpOnly Cookie │ ✅ 推荐,防止 JavaScript 访问 │ │
│ │ 内存变量 │ ✅ 推荐,刷新页面后丢失 │ │
│ └─────────────────┴─────────────────────────────────────┘ │
│ │
│ 移动应用: │
│ ┌─────────────────┬─────────────────────────────────────┐ │
│ │ 存储位置 │ 安全性评估 │ │
│ ├─────────────────┼─────────────────────────────────────┤ │
│ │ iOS Keychain │ ✅ 推荐,系统级加密存储 │ │
│ │ Android Keystore│ ✅ 推荐,硬件级安全存储 │ │
│ │ 本地文件 │ ❌ 不推荐,可能被读取 │ │
│ │ SharedPreferences│ ❌ 不推荐,无加密 │ │
│ └─────────────────┴─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
令牌传输 #
text
┌─────────────────────────────────────────────────────────────┐
│ 令牌传输安全 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ✅ 正确做法: │
│ │
│ HTTP Header 方式(推荐): │
│ Authorization: Bearer <token> │
│ │
│ ❌ 错误做法: │
│ │
│ URL 查询参数: │
│ GET /api/data?access_token=xxx │
│ (令牌会出现在日志、历史记录中) │
│ │
│ 请求体参数: │
│ POST /api/data │
│ { "access_token": "xxx" } │
│ (可能被缓存) │
│ │
└─────────────────────────────────────────────────────────────┘
令牌有效期 #
text
┌─────────────────────────────────────────────────────────────┐
│ 令牌有效期建议 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 访问令牌(Access Token): │
│ - 推荐有效期:15 分钟 - 1 小时 │
│ - 最长不超过:2 小时 │
│ - 理由:减少泄露后的影响范围 │
│ │
│ 刷新令牌(Refresh Token): │
│ - 推荐有效期:7 天 - 30 天 │
│ - 或:使用后轮换 │
│ - 理由:平衡安全性和用户体验 │
│ │
│ ID Token: │
│ - 推荐有效期:5 分钟 - 1 小时 │
│ - 理由:一次性验证使用 │
│ │
│ 授权码(Authorization Code): │
│ - 推荐有效期:30 秒 - 10 分钟 │
│ - 必须一次性使用 │
│ │
└─────────────────────────────────────────────────────────────┘
客户端安全 #
客户端认证 #
text
┌─────────────────────────────────────────────────────────────┐
│ 客户端认证安全 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 机密客户端: │
│ ✅ 使用 HTTP Basic 认证 │
│ ✅ 安全存储 client_secret │
│ ✅ 定期轮换 client_secret │
│ │
│ 公共客户端: │
│ ✅ 必须使用 PKCE │
│ ✅ 不使用 client_secret │
│ ✅ 验证 redirect_uri │
│ │
│ client_secret 存储: │
│ ✅ 环境变量 │
│ ✅ 密钥管理服务(AWS Secrets Manager, HashiCorp Vault) │
│ ❌ 代码仓库 │
│ ❌ 配置文件 │
│ │
└─────────────────────────────────────────────────────────────┘
客户端类型选择 #
text
┌─────────────────────────────────────────────────────────────┐
│ 客户端类型选择 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 机密客户端(Confidential Client): │
│ - Web 服务器应用 │
│ - 后端服务 │
│ - 可以安全存储 client_secret │
│ │
│ 公共客户端(Public Client): │
│ - 单页应用(SPA) │
│ - 原生移动应用 │
│ - 桌面应用 │
│ - 无法安全存储 client_secret │
│ │
│ 安全要求: │
│ - 公共客户端必须使用 PKCE │
│ - 机密客户端建议也使用 PKCE │
│ │
└─────────────────────────────────────────────────────────────┘
授权服务器安全 #
端点安全 #
text
┌─────────────────────────────────────────────────────────────┐
│ 授权服务器端点安全 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 所有端点: │
│ ✅ 强制 HTTPS │
│ ✅ 验证 TLS 证书 │
│ ✅ 防止点击劫持(X-Frame-Options) │
│ ✅ 内容安全策略(CSP) │
│ │
│ 授权端点: │
│ ✅ 验证 redirect_uri │
│ ✅ 验证 scope │
│ ✅ 使用 state 参数 │
│ ✅ 显示授权确认页面 │
│ │
│ 令牌端点: │
│ ✅ 客户端认证 │
│ ✅ 验证授权码 │
│ ✅ 验证 PKCE(如果使用) │
│ ✅ 速率限制 │
│ │
└─────────────────────────────────────────────────────────────┘
速率限制 #
javascript
const rateLimit = require('express-rate-limit');
const tokenLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: {
error: 'too_many_requests',
error_description: 'Too many token requests'
}
});
app.post('/token', tokenLimiter, handleTokenRequest);
Scope 安全 #
Scope 设计原则 #
text
┌─────────────────────────────────────────────────────────────┐
│ Scope 安全设计 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 最小权限原则 │
│ - 只授予必要的权限 │
│ - 默认授予最小权限 │
│ │
│ 2. 细粒度划分 │
│ - 按资源类型划分 │
│ - 按操作类型划分 │
│ │
│ 3. 用户知情 │
│ - 授权页面显示请求的权限 │
│ - 用户可以选择性授权 │
│ │
│ 4. 验证授权 │
│ - 资源服务器验证 scope │
│ - 拒绝超出授权范围的请求 │
│ │
└─────────────────────────────────────────────────────────────┘
Scope 验证 #
javascript
function verifyScope(requiredScopes, tokenScopes) {
const tokenScopeSet = new Set(tokenScopes.split(' '));
return requiredScopes.every(scope => tokenScopeSet.has(scope));
}
function scopeMiddleware(requiredScopes) {
return (req, res, next) => {
const tokenScope = req.token.scope;
if (!verifyScope(requiredScopes, tokenScope)) {
return res.status(403).json({
error: 'insufficient_scope',
error_description: 'Token does not have required scope'
});
}
next();
};
}
app.get('/api/admin/users',
scopeMiddleware(['admin:users:read']),
handleGetUsers
);
安全检查清单 #
客户端检查清单 #
text
□ 使用 HTTPS
□ 使用 state 参数并验证
□ 公共客户端使用 PKCE
□ 安全存储令牌
□ 不在 URL 中传递令牌
□ 验证 ID Token 签名
□ 验证 ID Token 声明(iss, aud, exp)
□ 使用短期令牌
□ 实现令牌刷新机制
□ 实现安全登出
授权服务器检查清单 #
text
□ 强制 HTTPS
□ 严格验证 redirect_uri
□ 使用 state 参数
□ 支持 PKCE
□ 授权码短期有效
□ 授权码一次性使用
□ 客户端认证
□ 速率限制
□ 安全存储 client_secret
□ 实现令牌撤销
□ 记录安全审计日志
资源服务器检查清单 #
text
□ 使用 HTTPS
□ 验证令牌签名
□ 验证令牌有效期
□ 验证令牌 scope
□ 实现速率限制
□ 不暴露敏感错误信息
□ 记录访问日志
安全响应头 #
javascript
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Strict-Transport-Security',
'max-age=31536000; includeSubDomains');
res.setHeader('Content-Security-Policy',
"default-src 'self'; script-src 'self'");
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
});
下一步 #
现在你已经了解了 OAuth 安全最佳实践,接下来学习 高级主题,了解更多 OAuth 的高级应用场景!
最后更新:2026-03-28