JWT 安全最佳实践 #

安全威胁概览 #

text
┌─────────────────────────────────────────────────────────────┐
│                    JWT 常见安全威胁                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  1. 算法混淆攻击                                     │   │
│  │     将 RS256 改为 HS256,用公钥作为密钥             │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  2. none 算法攻击                                    │   │
│  │     使用 alg: none 绕过签名验证                      │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  3. 弱密钥攻击                                       │   │
│  │     暴力破解弱密钥,伪造有效 Token                   │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  4. Token 泄露                                       │   │
│  │     Token 被窃取后冒充用户                           │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  5. 重放攻击                                         │   │
│  │     截获 Token 后重复使用                            │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  6. 信息泄露                                         │   │
│  │     敏感信息存储在 Payload 中                        │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

算法安全 #

明确指定算法 #

javascript
const jwt = require('jsonwebtoken');

const decoded = jwt.verify(token, publicKey, {
  algorithms: ['RS256']
});

拒绝不安全算法 #

text
┌─────────────────────────────────────────────────────────────┐
│                    算法安全配置                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  推荐算法:                                                  │
│  ✅ RS256 - RSA 签名,广泛支持                              │
│  ✅ ES256 - 椭圆曲线,性能好                                │
│  ✅ PS256 - RSA-PSS,更安全                                 │
│                                                             │
│  谨慎使用:                                                  │
│  ⚠️ HS256 - 需要安全密钥管理                                │
│                                                             │
│  禁止使用:                                                  │
│  ❌ none - 无签名,极度危险                                  │
│  ❌ HS256 弱密钥 - 容易被破解                               │
│                                                             │
│  验证代码:                                                  │
│  const allowedAlgorithms = ['RS256', 'ES256'];              │
│  if (!allowedAlgorithms.includes(header.alg)) {             │
│    throw new Error('Algorithm not allowed');                │
│  }                                                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

防止算法混淆攻击 #

javascript
const jwt = require('jsonwebtoken');

function verifyToken(token, publicKey) {
  const decoded = jwt.verify(token, publicKey, {
    algorithms: ['RS256', 'ES256']
  });
  
  return decoded;
}

密钥安全 #

密钥强度要求 #

text
┌─────────────────────────────────────────────────────────────┐
│                    密钥强度要求                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  HMAC 密钥(HS256/HS384/HS512):                           │
│  ─────────────────────────────────────────────────────────  │
│  - HS256:至少 32 字节(256 位)                           │
│  - HS384:至少 48 字节(384 位)                           │
│  - HS512:至少 64 字节(512 位)                           │
│  - 推荐:使用加密安全的随机数生成                           │
│                                                             │
│  RSA 密钥:                                                 │
│  ─────────────────────────────────────────────────────────  │
│  - 最小:2048 位                                            │
│  - 推荐:3072 位或更高                                      │
│  - 高安全:4096 位                                          │
│                                                             │
│  ECDSA 密钥:                                               │
│  ─────────────────────────────────────────────────────────  │
│  - ES256:P-256 曲线                                        │
│  - ES384:P-384 曲线                                        │
│  - ES512:P-521 曲线                                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

生成安全密钥 #

javascript
const crypto = require('crypto');

const hmacSecret = crypto.randomBytes(32).toString('hex');

const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
  modulusLength: 3072,
  publicKeyEncoding: { type: 'spki', format: 'pem' },
  privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});

const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', {
  namedCurve: 'P-256',
  publicKeyEncoding: { type: 'spki', format: 'pem' },
  privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});

密钥存储 #

text
┌─────────────────────────────────────────────────────────────┐
│                    密钥存储最佳实践                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ❌ 错误做法:                                              │
│  - 将密钥硬编码在代码中                                     │
│  - 将密钥提交到版本控制                                     │
│  - 在日志中打印密钥                                         │
│  - 使用弱密钥(如 "secret", "123456")                      │
│                                                             │
│  ✅ 正确做法:                                              │
│  - 使用环境变量存储密钥                                     │
│  - 使用密钥管理服务(KMS)                                  │
│  - 使用硬件安全模块(HSM)                                  │
│  - 定期轮换密钥                                             │
│  - 不同环境使用不同密钥                                     │
│                                                             │
│  环境变量示例:                                              │
│  JWT_SECRET=your-secure-random-key-here                     │
│  JWT_PRIVATE_KEY_PATH=/secure/keys/private.pem             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

密钥轮换 #

javascript
const jwt = require('jsonwebtoken');

const keyStore = {
  'key-2024-01': fs.readFileSync('keys/2024-01/private.pem'),
  'key-2024-02': fs.readFileSync('keys/2024-02/private.pem')
};

const currentKeyId = 'key-2024-02';

function signToken(payload) {
  return jwt.sign(payload, keyStore[currentKeyId], {
    algorithm: 'RS256',
    keyid: currentKeyId
  });
}

function verifyToken(token) {
  const decoded = jwt.decode(token, { complete: true });
  const kid = decoded.header.kid;
  
  if (!keyStore[kid]) {
    throw new Error('Unknown key ID');
  }
  
  return jwt.verify(token, keyStore[kid], {
    algorithms: ['RS256']
  });
}

Token 过期策略 #

推荐过期时间 #

text
┌─────────────────────────────────────────────────────────────┐
│                    Token 过期时间建议                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  访问令牌(Access Token):                                  │
│  ─────────────────────────────────────────────────────────  │
│  - 网页应用:15-30 分钟                                      │
│  - 移动应用:30-60 分钟                                      │
│  - 高安全应用:5-15 分钟                                     │
│                                                             │
│  刷新令牌(Refresh Token):                                 │
│  ─────────────────────────────────────────────────────────  │
│  - 网页应用:7-14 天                                         │
│  - 移动应用:30-90 天                                        │
│  - 高安全应用:1-7 天                                        │
│                                                             │
│  ID Token(OpenID Connect):                               │
│  ─────────────────────────────────────────────────────────  │
│  - 通常:5-15 分钟                                           │
│  - 仅用于身份认证                                           │
│                                                             │
│  原则:                                                     │
│  - 访问令牌短,刷新令牌长                                    │
│  - 敏感操作使用更短过期时间                                  │
│  - 平衡安全性和用户体验                                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

实现过期策略 #

javascript
const jwt = require('jsonwebtoken');

function generateTokens(user) {
  const accessToken = jwt.sign(
    { sub: user.id, role: user.role },
    process.env.JWT_SECRET,
    { 
      algorithm: 'HS256',
      expiresIn: '15m',
      issuer: 'https://auth.example.com'
    }
  );
  
  const refreshToken = jwt.sign(
    { sub: user.id, type: 'refresh' },
    process.env.JWT_REFRESH_SECRET,
    { 
      algorithm: 'HS256',
      expiresIn: '7d'
    }
  );
  
  return { accessToken, refreshToken };
}

Token 存储安全 #

客户端存储方式对比 #

text
┌─────────────────────────────────────────────────────────────┐
│                    Token 存储方式对比                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  localStorage:                                              │
│  ─────────────────────────────────────────────────────────  │
│  ❌ 容易受 XSS 攻击                                         │
│  ❌ 任何 JavaScript 都可访问                                │
│  ✅ 实现简单                                                │
│  ⚠️ 不推荐用于敏感 Token                                   │
│                                                             │
│  sessionStorage:                                            │
│  ─────────────────────────────────────────────────────────  │
│  ❌ 容易受 XSS 攻击                                         │
│  ✅ 关闭标签页后清除                                        │
│  ⚠️ 比 localStorage 稍安全                                 │
│                                                             │
│  HttpOnly Cookie:                                           │
│  ─────────────────────────────────────────────────────────  │
│  ✅ 防止 JavaScript 访问                                    │
│  ✅ 防止 XSS 窃取                                           │
│  ⚠️ 需要防止 CSRF                                          │
│  ✅ 推荐用于访问令牌                                        │
│                                                             │
│  内存(Memory):                                            │
│  ─────────────────────────────────────────────────────────  │
│  ✅ 最安全(页面关闭后清除)                                │
│  ❌ 刷新页面后丢失                                          │
│  ✅ 适合单页应用                                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘
javascript
app.post('/login', (req, res) => {
  const { accessToken, refreshToken } = generateTokens(user);
  
  res.cookie('accessToken', accessToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 15 * 60 * 1000
  });
  
  res.cookie('refreshToken', refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    path: '/auth/refresh',
    maxAge: 7 * 24 * 60 * 60 * 1000
  });
  
  res.json({ message: 'Login successful' });
});

CSRF 防护 #

javascript
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });

app.get('/api/csrf-token', csrfProtection, (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

app.post('/api/sensitive-action', csrfProtection, (req, res) => {
  res.json({ message: 'Action completed' });
});

Token 撤销机制 #

为什么需要撤销? #

text
┌─────────────────────────────────────────────────────────────┐
│                    Token 撤销场景                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  需要撤销 Token 的场景:                                     │
│                                                             │
│  1. 用户主动登出                                            │
│     - 用户点击"退出登录"                                    │
│     - 需要立即使 Token 失效                                 │
│                                                             │
│  2. 安全事件                                                │
│     - 检测到异常登录                                        │
│     - 账户被锁定                                            │
│     - 密码被修改                                            │
│                                                             │
│  3. 权限变更                                                │
│     - 用户角色改变                                          │
│     - 权限被撤销                                            │
│                                                             │
│  4. Token 泄露                                              │
│     - Token 被窃取                                          │
│     - 安全漏洞被发现                                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

黑名单机制 #

javascript
const redis = require('redis');
const client = redis.createClient();

async function revokeToken(token) {
  const decoded = jwt.decode(token);
  const ttl = decoded.exp - Math.floor(Date.now() / 1000);
  
  if (ttl > 0) {
    await client.setex(`blacklist:${token}`, ttl, '1');
  }
}

async function isTokenRevoked(token) {
  const result = await client.get(`blacklist:${token}`);
  return result !== null;
}

async function verifyToken(token) {
  if (await isTokenRevoked(token)) {
    throw new Error('Token has been revoked');
  }
  
  return jwt.verify(token, process.env.JWT_SECRET);
}

版本号机制 #

javascript
const userTokenVersions = new Map();

function generateToken(user) {
  const version = userTokenVersions.get(user.id) || 0;
  
  return jwt.sign(
    { sub: user.id, version },
    process.env.JWT_SECRET,
    { expiresIn: '1h' }
  );
}

function revokeAllTokens(userId) {
  const currentVersion = userTokenVersions.get(userId) || 0;
  userTokenVersions.set(userId, currentVersion + 1);
}

function verifyToken(token) {
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  const currentVersion = userTokenVersions.get(decoded.sub) || 0;
  
  if (decoded.version !== currentVersion) {
    throw new Error('Token has been revoked');
  }
  
  return decoded;
}

短有效期 + 刷新令牌 #

text
┌─────────────────────────────────────────────────────────────┐
│                    短有效期策略                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  原理:                                                     │
│  - 访问令牌有效期很短(如 15 分钟)                          │
│  - 使用刷新令牌获取新的访问令牌                              │
│  - 刷新令牌存储在服务端,可随时撤销                          │
│                                                             │
│  优势:                                                     │
│  ✅ 即使 Token 泄露,影响时间有限                           │
│  ✅ 可以在刷新时检查用户状态                                │
│  ✅ 实现相对简单                                            │
│                                                             │
│  流程:                                                     │
│  1. 用户登录,获取访问令牌和刷新令牌                        │
│  2. 访问令牌过期后,用刷新令牌获取新的                       │
│  3. 服务端可以在刷新时拒绝(如用户被禁用)                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Payload 安全 #

不要存储敏感信息 #

text
┌─────────────────────────────────────────────────────────────┐
│                    Payload 安全规则                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ❌ 不要存储:                                              │
│  - 密码(即使是哈希后的)                                   │
│  - 信用卡号                                                 │
│  - 身份证号                                                 │
│  - 社会安全号                                               │
│  - API 密钥                                                 │
│  - 私密个人信息                                             │
│                                                             │
│  ✅ 可以存储:                                              │
│  - 用户 ID                                                  │
│  - 用户名                                                   │
│  - 角色/权限                                                │
│  - 过期时间                                                 │
│  - 非敏感的用户偏好                                         │
│                                                             │
│  原因:                                                     │
│  - JWT Payload 只做 Base64 编码,不加密                     │
│  - 任何人都可以解码查看内容                                 │
│  - 敏感信息应该存储在服务端                                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘

最小权限原则 #

javascript
const payload = {
  sub: 'user-123',
  role: 'user',
  permissions: ['read:profile', 'read:orders']
};

const token = jwt.sign(payload, secret, { expiresIn: '1h' });

传输安全 #

使用 HTTPS #

text
┌─────────────────────────────────────────────────────────────┐
│                    HTTPS 强制要求                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ⚠️ JWT 必须通过 HTTPS 传输                                 │
│                                                             │
│  原因:                                                     │
│  - HTTP 明文传输,Token 可被截获                            │
│  - 中间人攻击可窃取 Token                                   │
│  - HTTPS 加密整个通信过程                                   │
│                                                             │
│  配置示例(Express):                                       │
│  app.use((req, res, next) => {                              │
│    if (!req.secure && req.get('x-forwarded-proto') !== 'https') { │
│      return res.redirect('https://' + req.get('host') + req.url); │
│    }                                                        │
│    next();                                                  │
│  });                                                        │
│                                                             │
│  安全头配置:                                               │
│  app.use(helmet());                                         │
│  app.use(hsts({ maxAge: 31536000 }));                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

安全头设置 #

javascript
const helmet = require('helmet');

app.use(helmet());

app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", 'data:', 'https:'],
    connectSrc: ["'self'", 'https://api.example.com'],
    fontSrc: ["'self'"],
    objectSrc: ["'none'"],
    frameSrc: ["'none'"],
    upgradeInsecureRequests: []
  }
}));

app.use(helmet.xssFilter());
app.use(helmet.noSniff());
app.use(helmet.referrerPolicy({ policy: 'strict-origin-when-cross-origin' }));

声明验证 #

完整验证清单 #

javascript
function verifyToken(token, options = {}) {
  const decoded = jwt.verify(token, options.secret || process.env.JWT_SECRET, {
    algorithms: options.algorithms || ['RS256'],
    issuer: options.issuer,
    audience: options.audience
  });
  
  if (decoded.iss !== options.issuer) {
    throw new Error('Invalid issuer');
  }
  
  if (!decoded.aud.includes(options.audience)) {
    throw new Error('Invalid audience');
  }
  
  const now = Math.floor(Date.now() / 1000);
  
  if (decoded.exp && decoded.exp < now) {
    throw new Error('Token expired');
  }
  
  if (decoded.nbf && decoded.nbf > now) {
    throw new Error('Token not yet valid');
  }
  
  if (decoded.iat && decoded.iat > now + 60) {
    throw new Error('Token issued in the future');
  }
  
  return decoded;
}

验证配置示例 #

javascript
const verifyOptions = {
  secret: process.env.JWT_PUBLIC_KEY,
  algorithms: ['RS256'],
  issuer: 'https://auth.example.com',
  audience: 'https://api.example.com',
  clockTolerance: 10,
  maxAge: '1h'
};

安全检查清单 #

text
┌─────────────────────────────────────────────────────────────┐
│                    JWT 安全检查清单                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  算法安全                                                   │
│  □ 明确指定允许的算法列表                                   │
│  □ 禁止 none 算法                                          │
│  □ 使用强签名算法(RS256/ES256)                            │
│                                                             │
│  密钥安全                                                   │
│  □ 使用足够强度的密钥                                       │
│  □ 密钥安全存储(环境变量/KMS)                             │
│  □ 定期轮换密钥                                             │
│  □ 不同环境使用不同密钥                                     │
│                                                             │
│  Token 安全                                                 │
│  □ 设置合理的过期时间                                       │
│  □ 不存储敏感信息                                           │
│  □ 实现撤销机制                                             │
│  □ 使用刷新令牌                                             │
│                                                             │
│  传输安全                                                   │
│  □ 强制使用 HTTPS                                          │
│  □ 设置安全头                                               │
│  □ 使用 HttpOnly Cookie                                    │
│  □ 配置 CSRF 防护                                          │
│                                                             │
│  验证安全                                                   │
│  □ 验证签名                                                 │
│  □ 验证 iss 声明                                           │
│  □ 验证 aud 声明                                           │
│  □ 验证 exp 声明                                           │
│  □ 验证 nbf 声明                                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

下一步 #

现在你已经了解了 JWT 安全最佳实践,接下来学习 JWT 高级主题,探索 JWKS、嵌套 JWT、加密等高级功能!

最后更新:2026-03-28