JSON Web Token(JWT)是当今最流行的 API 身份验证方案之一。据 Auth0 2025 年的报告,超过 78% 的 REST API 使用 JWT 作为认证机制。然而,OWASP 的审计数据显示,超过 60% 的 JWT 实现存在至少一个安全漏洞。这不是 JWT 本身不安全,而是大多数开发者在实现时踩了坑。
本文将深入剖析 JWT 最常见的安全漏洞,给出可运行的攻击演示和防御代码,并总结一套可直接落地的最佳实践。
🔐 一、JWT 基础与常见攻击手法
1.1 JWT 结构速览
一个 JWT 由三部分组成:Header(头部)、Payload(载荷)、Signature(签名),用 . 连接。每个部分都是 Base64URL 编码的 JSON。
// JWT 结构示意
// eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjF9.signature
// |______ Header _______|_|___ Payload ___|_|_ Signature _|
📌 记住: Base64URL 编码不是加密。JWT 的 Header 和 Payload 对任何人都是可读的,绝对不要在其中存放敏感信息(如密码、身份证号)。
1.2 🔥 算法混淆攻击(Algorithm Confusion)
这是 JWT 最经典的漏洞,2015 年由 Tim McLean 首次披露,至今仍在生产环境中被发现。
攻击原理: JWT 的 Header 中有个 alg 字段,声称使用什么算法签名。如果服务器没有严格校验 alg,攻击者可以把 alg 从 HS256 改为 RS256,然后用 RSA 公钥(通常是公开的)作为 HMAC 密钥来伪造签名。
// ❌ 危险:服务器未校验算法类型
const jwt = require('jsonwebtoken');
function verifyToken(token) {
// 这行代码存在算法混淆漏洞!
// 攻击者可以将 alg 改为 HS256,用公钥签名
return jwt.verify(token, publicKey); // publicKey 是 RSA 公钥
}
// ✅ 安全:显式指定允许的算法
const jwt = require('jsonwebtoken');
function verifyToken(token) {
return jwt.verify(token, publicKey, {
algorithms: ['RS256'] // 严格限定算法,拒绝其他一切
});
}
攻击演示:
// 攻击者伪造 Token 的完整流程
const jwt = require('jsonwebtoken');
const fs = require('fs');
// 1. 获取公开的 RSA 公钥(通常在 JWKS 端点公开)
const publicKey = fs.readFileSync('public.pem');
// 2. 构造恶意 payload
const payload = { userId: 1, role: 'admin' };
// 3. 用 RSA 公钥作为 HMAC 密钥签名,指定 alg 为 HS256
const fakeToken = jwt.sign(payload, publicKey, { algorithm: 'HS256' });
// 4. 如果服务器未校验算法,这个 Token 就能通过验证!
console.log('伪造 Token:', fakeToken);
⚠️ 警告: 如果你的系统使用 RSA/ECDSA 非对称加密,务必在验证时用
algorithms白名单限定算法。这是最常见的高危漏洞。
1.3 🔥 alg: none 攻击
JWT 规范允许 alg 为 none,表示无签名。许多库默认信任 Header 中的 alg 字段。
// 攻击者构造 alg:none 的 Token
const header = Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' })).toString('base64url');
const payload = Buffer.from(JSON.stringify({ userId: 1, role: 'admin' })).toString('base64url');
// 签名部分留空
const fakeToken = `${header}.${payload}.`;
console.log('无签名 Token:', fakeToken);
// eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VySWQiOjEsInJvbGUiOiJhZG1pbiJ9.
防御方案:
# Python (PyJWT) 安全配置
import jwt
def verify_token(token: str) -> dict:
# ✅ 拒绝 none 算法,只接受 HS256
return jwt.decode(
token,
SECRET_KEY,
algorithms=['HS256'], # 白名单,绝不包含 none
options={
'verify_signature': True,
'verify_exp': True,
'require': ['exp', 'iat', 'sub'] # 强制要求这些字段
}
)
🚀 二、密钥管理与 Token 生命周期
2.1 密钥泄露:最致命的安全事故
JWT 的安全性完全依赖于签名密钥。一旦 HMAC 密钥泄露,攻击者可以伪造任意 Token。
| 密钥管理方式 | 安全等级 | 推荐场景 | 风险说明 |
|---|---|---|---|
| 硬编码在源码 | ❌ 极低 | 绝不推荐 | 提交到 Git 后等于公开 |
| 环境变量 | ⚠️ 中等 | 小型项目 | 容器日志可能泄露 |
| 密钥管理服务 (KMS) | ✅ 高 | 企业级 | 推荐 AWS KMS / HashiCorp Vault |
| 非对称加密 (RS256) | ✅ 高 | 微服务架构 | 私钥签名,公钥验证,公钥泄露无影响 |
// ❌ 绝对不要这样做
const SECRET_KEY = 'my-super-secret-key-123'; // 硬编码在代码中
const token = jwt.sign(payload, SECRET_KEY);
// ✅ 正确做法:使用环境变量 + 长随机密钥
// 生成密钥:node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
const SECRET_KEY = process.env.JWT_SECRET;
if (!SECRET_KEY || SECRET_KEY.length < 32) {
throw new Error('JWT_SECRET 未设置或长度不足');
}
const token = jwt.sign(payload, SECRET_KEY, { algorithm: 'HS256' });
💡 提示: HMAC 密钥长度至少 256 位(32 字节)。使用
crypto.randomBytes(64).toString('hex')生成 128 字符的随机密钥。
2.2 Token 过期策略
永不过期的 Token 等于永不失效的密码。合理的过期策略是安全的基石。
| Token 类型 | 过期时间 | 用途 | 存储位置 |
|---|---|---|---|
| Access Token | 15-30 分钟 | API 请求认证 | 内存 / httpOnly Cookie |
| Refresh Token | 7-30 天 | 刷新 Access Token | httpOnly Cookie(服务端存储) |
| 长期 Token | 1-12 个月 | 第三方集成 / API Key | 数据库(可撤销) |
// 完整的 Token 刷新机制
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
// 生成 Token 对
function generateTokenPair(user) {
const accessToken = jwt.sign(
{ sub: user.id, role: user.role },
ACCESS_SECRET,
{ expiresIn: '15m', algorithm: 'HS256' } // 短期 Access Token
);
const refreshToken = crypto.randomBytes(40).toString('hex');
// Refresh Token 存入数据库,支持主动撤销
db.refreshTokens.create({
token: refreshToken,
userId: user.id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
return { accessToken, refreshToken };
}
// 刷新 Token
async function refreshTokens(refreshToken) {
// 1. 验证 Refresh Token 存在且未过期
const record = await db.refreshTokens.findOne({
where: { token: refreshToken, expiresAt: { $gt: new Date() } }
});
if (!record) throw new Error('Invalid refresh token');
// 2. 旋转 Refresh Token(用一次就换新的)
await db.refreshTokens.destroy({ where: { id: record.id } });
// 3. 生成新的 Token 对
const user = await db.users.findByPk(record.userId);
return generateTokenPair(user);
}
⚠️ 警告: Refresh Token 必须支持旋转(Rotation)——每次刷新都生成新的 Refresh Token,并使旧的失效。这可以限制 Refresh Token 被盗后的影响范围。
2.3 Token 吊销(黑名单机制)
JWT 是无状态的,天然不支持主动吊销。但实际业务中,用户注销、密码修改、管理员封禁都需要立即失效 Token。
// Redis 黑名单方案
const Redis = require('ioredis');
const redis = new Redis();
async function revokeToken(token) {
const decoded = jwt.decode(token);
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
// 用 Token 的 jti(唯一标识)加入黑名单,设置与过期时间一致的 TTL
await redis.setex(`blacklist:${decoded.jti}`, ttl, 'revoked');
}
}
async function verifyWithBlacklist(token) {
const decoded = jwt.verify(token, SECRET_KEY, { algorithms: ['HS256'] });
// 检查是否在黑名单中
const isRevoked = await redis.get(`blacklist:${decoded.jti}`);
if (isRevoked) throw new Error('Token has been revoked');
return decoded;
}
💡 三、生产环境安全检查清单
3.1 十大常见安全反模式
以下是我在代码审计中反复遇到的问题:
| # | 反模式 | 危险等级 | 正确做法 |
|---|---|---|---|
| 1 | 不验证 alg 字段 |
🔴 高危 | 使用 algorithms 白名单 |
| 2 | HMAC 密钥硬编码 | 🔴 高危 | 环境变量或 KMS |
| 3 | 密钥长度不足 256 位 | 🟡 中危 | 至少 32 字节随机密钥 |
| 4 | 在 Payload 放敏感数据 | 🟡 中危 | 只放 userId 等标识符 |
| 5 | Token 永不过期 | 🔴 高危 | Access Token 15-30 分钟 |
| 6 | 不校验 iss / aud |
🟡 中危 | 校验签发者和受众 |
| 7 | Refresh Token 不旋转 | 🟡 中危 | 每次刷新都换新 Token |
| 8 | 将 Token 存在 localStorage | 🟡 中危 | httpOnly Cookie 防 XSS |
| 9 | 不记录 jti 无法吊销 |
🟠 中低危 | 使用 jti + Redis 黑名单 |
| 10 | 日志中打印完整 Token | 🟠 中低危 | 只记录 Token 前 8 位 |
3.2 完整的安全验证中间件
// Express.js 生产级 JWT 验证中间件
const jwt = require('jsonwebtoken');
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
const SECRET = process.env.JWT_SECRET;
const jwtMiddleware = async (req, res, next) => {
try {
// 1. 从 Authorization Header 提取 Token
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
const token = authHeader.slice(7);
// 2. 验证签名 + 过期时间 + 算法白名单
const decoded = jwt.verify(token, SECRET, {
algorithms: ['HS256'], // 限定算法
issuer: 'jsjson.com', // 校验签发者
audience: 'api.jsjson.com', // 校验受众
clockTolerance: 30 // 30 秒时钟偏差容忍
});
// 3. 检查黑名单(Token 吊销)
if (decoded.jti) {
const revoked = await redis.get(`bl:${decoded.jti}`);
if (revoked) return res.status(401).json({ error: 'Token revoked' });
}
// 4. 注入用户信息到请求对象
req.user = { id: decoded.sub, role: decoded.role };
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
}
return res.status(401).json({ error: 'Invalid token' });
}
};
// 用法
app.get('/api/profile', jwtMiddleware, (req, res) => {
res.json({ userId: req.user.id });
});
3.3 存储方案对比
Token 存在哪里,直接决定了被 XSS 攻击的风险:
| 存储方式 | XSS 风险 | CSRF 风险 | 推荐指数 |
|---|---|---|---|
localStorage |
🔴 高(JS 可读取) | 🟢 低 | ❌ 不推荐 |
sessionStorage |
🔴 高(同源 JS 可读) | 🟢 低 | ❌ 不推荐 |
httpOnly Cookie |
🟢 低(JS 不可读) | 🟡 中(需 CSRF Token) | ✅ 推荐 |
| 内存变量 | 🟢 低(页面刷新丢失) | 🟢 低 | ⚠️ SPA 可用 |
💡 提示: 最佳方案是将 JWT 存在
httpOnly+Secure+SameSite=StrictCookie 中,配合 CSRF Token 防御跨站请求伪造。
✅ 总结与建议
JWT 安全不是一个开关,而是一组需要同时满足的条件。以下是按优先级排列的核心建议:
- 强制验证算法白名单 —
algorithms: ['RS256']或['HS256'],绝不信任 Header 中的alg - 使用足够长的随机密钥 — HMAC 至少 256 位,推荐 512 位
- 短期 Access Token + 可刷新机制 — 15 分钟过期 + Refresh Token 旋转
- 实现 Token 吊销 — 使用
jti+ Redis 黑名单 - httpOnly Cookie 存储 — 防止 XSS 窃取 Token
- 校验
iss、aud、exp— 限制 Token 的使用范围 - 不要在 Payload 放敏感信息 — Payload 只是 Base64 编码,不是加密
⚡ 关键结论: JWT 的安全性取决于最薄弱的实现环节。算法混淆、密钥泄露、永不过期这三个问题占了 JWT 安全事故的 80% 以上。把这三个解决了,你就已经超越了大多数系统。
相关工具推荐: 使用 jsjson.com 的 在线 JWT 解析工具 可以快速解码 JWT 的 Header 和 Payload,排查 Token 结构问题。配合 SHA256 哈希工具 和 Base64 编解码工具,可以完成常见的安全调试工作。