云计算百科
云计算领域专业知识百科平台

JWT最佳实践:10 条避坑指南

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 做自检也安全。建议把这份速查表存下来,每次新项目上线前走一遍。

赞(0)
未经允许不得转载:网硕互联帮助中心 » JWT最佳实践:10 条避坑指南
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!