JWT 用起来简单,但用到生产环境有不少坑。密钥太短被爆破、alg: none 绕过验证、Token 泄露无法撤销——这些问题在开发阶段感觉不到,上线后被攻击者打脸。本文整理 10 条经过生产验证的 JWT 最佳实践,每条配正确和错误示例对照。
1. 密钥长度至少 256 位
HS256 的密钥必须至少 256 位(32 字节)。短密钥会被暴力破解。
const crypto = require('crypto')
// 错误:弱密钥
jwt.sign(payload, 'secret', { algorithm: 'HS256' })
jwt.sign(payload, '123456', { algorithm: 'HS256' })
jwt.sign(payload, 'my-secret-key', { algorithm: 'HS256' })
// 正确:随机生成 256 位密钥
const secret = crypto.randomBytes(32).toString('hex') // 64 字符
jwt.sign(payload, secret, { algorithm: 'HS256' })
从环境变量读取密钥,不硬编码在代码中:
const secret = process.env.JWT_SECRET
if (!secret || secret.length < 32) {
throw new Error('JWT_SECRET must be at least 32 characters')
}
2. 永远拒绝 alg: none
alg: none 的 Token 没有签名,任何人都能伪造。验签时必须指定算法白名单。
// 错误:不指定算法,可能被 alg: none 绕过
const decoded = jwt.verify(token, secret)
// 正确:算法白名单,不包含 none
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256']
})
3. 验证 iss/aud/sub 声明
只验签是不够的,还要验证 Token 来自正确的签发方、发送给正确的接收方。
// 错误:只验签,不校验声明
const decoded = jwt.verify(token, secret)
// 正确:校验签发者和接收方
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256'],
issuer: 'https://auth.myapp.com', // iss
audience: 'myapp-web' // aud
})
如果你的 Token 被其他系统签发(如 Auth0),iss 验证能防止 Token 跨系统混用。aud 验证能防止 Web 端 Token 被用于 API 端。
4. Access Token 有效期不超过 15 分钟
短时 Token + Refresh Token 是最安全的方案。
// 错误:Access Token 有效期太长
jwt.sign(payload, secret, { expiresIn: '7d' })
// 正确:Access Token 15 分钟 + Refresh Token 7 天
const accessToken = jwt.sign(payload, ACCESS_SECRET, { expiresIn: '15m' })
const refreshToken = jwt.sign({ sub: userId, type: 'refresh' }, REFRESH_SECRET, { expiresIn: '7d' })
15 分钟的泄露窗口意味着,即使 Token 被截获,攻击者最多只有 15 分钟可用。配合 Refresh Token 刷新,用户体验不受影响。
5. 不在 Payload 存储敏感信息
Payload 是 Base64URL 编码,不是加密。任何拿到 Token 的人都能解码。
// 错误:Payload 放敏感信息
jwt.sign({
userId: 10086,
email: 'zhangsan@company.com',
phone: '13800138000',
role: 'admin'
}, secret, { expiresIn: '15m' })
// 正确:Payload 只放必要标识
jwt.sign({
sub: '10086',
role: 'admin'
}, secret, { expiresIn: '15m' })
// 需要详细信息时从数据库查询
6. 使用 kid 字段支持密钥轮换
密钥泄露后需要立即换新密钥,但换密钥后旧 Token 全部失效。用 kid 字段可以让新旧密钥并存。
// 签发时指定 kid
jwt.sign(payload, currentSecret, {
algorithm: 'HS256',
keyid: 'key-2026-08' // 当前密钥 ID
})
// 验证时根据 kid 选择密钥
const keyMap = {
'key-2026-08': process.env.JWT_SECRET_2026_08,
'key-2026-06': process.env.JWT_SECRET_2026_06, // 旧密钥,过期前仍可验证
}
function verifyToken(token) {
const decoded = jwt.decode(token, { complete: true }) // 先解码 Header
const kid = decoded.header.kid
const secret = keyMap[kid]
if (!secret) throw new Error('Unknown key ID')
return jwt.verify(token, secret, {
algorithms: ['HS256'],
issuer: 'https://auth.myapp.com'
})
}
密钥轮换流程:签发新密钥 → 新旧密钥并存验证 → 旧 Token 逐渐过期 → 移除旧密钥。
7. HTTPS 传输防止 Token 被截获
Token 在 HTTP 明文传输时,中间人可以截获。
# Nginx 强制 HTTPS
server {
listen 80;
server_name api.myapp.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name api.myapp.com;
ssl_certificate /etc/ssl/myapp.crt;
ssl_certificate_key /etc/ssl/myapp.key;
ssl_protocols TLSv1.2 TLSv1.3;
}
开启 HSTS 防止降级攻击:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
8. Cookie 设置 HttpOnly + Secure + SameSite
如果用 Cookie 存储 Token,三个属性缺一不可:
// 设置 Cookie
res.cookie('accessToken', token, {
httpOnly: true, // JS 无法读取,防 XSS
secure: true, // 仅 HTTPS 传输
sameSite: 'strict', // 防 CSRF
maxAge: 15 * 60 * 1000, // 15 分钟
path: '/'
})
| httpOnly | XSS 读取 Cookie | 攻击者可用 JS 偷 Token |
| secure | 中间人截获 | HTTP 明文传输 Token |
| sameSite | CSRF 跨站请求 | 第三方网站可携带 Cookie 请求 |
9. 日志中不记录完整 Token
生产日志中完整记录 Token 等于泄露 Token。
// 错误:日志记录完整 Token
app.use((req, res, next) => {
console.log(`Auth: ${req.headers.authorization}`)
next()
})
// 日志输出: Auth: Bearer eyJhbGciOiJIUzI1NiIs…
// 正确:只记录用户 ID 和操作
app.use((req, res, next) => {
const auth = req.headers.authorization
if (auth?.startsWith('Bearer ')) {
try {
const decoded = jwt.decode(auth.slice(7)) // 不验签,只解码
console.log(`User ${decoded.sub} ${req.method} ${req.path}`)
} catch {
console.log(`Invalid token ${req.method} ${req.path}`)
}
}
next()
})
// 日志输出: User 10086 GET /api/profile
jwt.decode 只解码不验签,不验证签名。它用于读取 Payload 信息(如 sub),不做认证。认证用 jwt.verify。
10. 定期轮换签名密钥
即使密钥没泄露,定期轮换也是安全最佳实践。建议每 3-6 个月轮换一次。
// 密钥轮换任务
const crypto = require('crypto')
function generateNewKey() {
return {
kid: `key-${new Date().toISOString().slice(0, 7)}`, // key-2026-08
secret: crypto.randomBytes(32).toString('hex')
}
}
// 轮换步骤:
// 1. 生成新密钥,加入 keyMap
// 2. 签发 Token 使用新密钥
// 3. 验证 Token 时新旧密钥都支持
// 4. 等待旧 Token 全部过期(= 旧 Token 的最大 exp)
// 5. 从 keyMap 移除旧密钥
实践速查表
| 1 | 密钥至少 256 位 | 暴力破解 | crypto.randomBytes(32) |
| 2 | 拒绝 alg: none | 无签名伪造 | algorithms: ['HS256'] |
| 3 | 验证 iss/aud/sub | Token 混用 | issuer + audience |
| 4 | exp 不超过 15 分钟 | 泄露窗口长 | 短时 + Refresh Token |
| 5 | Payload 不放敏感信息 | 信息泄露 | 只放 sub + role |
| 6 | 使用 kid 轮换 | 密钥泄露无退路 | 多密钥并存 |
| 7 | HTTPS 传输 | 中间人截获 | 强制 HTTPS + HSTS |
| 8 | Cookie 三件套 | XSS / CSRF | HttpOnly + Secure + SameSite |
| 9 | 日志脱敏 | Token 泄露 | 只记 sub + 操作 |
| 10 | 定期轮换密钥 | 长期泄露风险 | 3-6 个月换密钥 |
在线自检
上线前用你的生产 Token 做一次自检。可以在 盘子工具站 JWT 解析工具 上做这种检查——粘贴 Token 后逐项对照本文的 10 条实践: 
- Header 卡片中是否有 kid 字段?(第 6 条)
- Payload 卡片中是否有 email、phone 等敏感信息?(第 5 条)
- Payload 卡片下方的过期时间是否在 15 分钟以内?(第 4 条)
- 算法徽章显示的 alg 是否在算法参考表中标注"推荐"?(第 1、2 条)
- "有效"徽章确认 Token 当前未过期(第 4 条)
工具的算法参考表直接列出了 10 种 JWT 算法的安全性标注。如果你的 Token 用的是 none,会显示醒目的橙色警告。所有解码在浏览器本地完成,不上传服务器,用真实 Token 做自检也安全。建议把这份速查表存下来,每次新项目上线前走一遍。
网硕互联帮助中心






评论前必须登录!
注册