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