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): │
│ ───────────────────────────────────────────────────────── │
│ ✅ 最安全(页面关闭后清除) │
│ ❌ 刷新页面后丢失 │
│ ✅ 适合单页应用 │
│ │
└─────────────────────────────────────────────────────────────┘
使用 HttpOnly Cookie #
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