JSON Web Token 安全:常见漏洞与修复方案
JWT 是 API 认证的事实标准,但其表面上的简单性隐藏了真正的安全隐患。配置不当的 JWT 曾导致认证绕过、权限提升和数据泄露。本指南涵盖最关键的漏洞以及如何防范它们。
JWT 结构回顾
JWT 由三个 Base64URL 编码的部分组成,以点号分隔:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMiLCJyb2xlIjoiYWRtaW4ifQ.signature
- Header(头部):算法和令牌类型
- Payload(载荷):声明(用户数据、过期时间等)
- Signature(签名):加密验证
使用我们的 JWT 编码器/解码器 检查 JWT 结构。
如需了解 JWT 结构的基础知识,请参阅我们的 JWT 令牌详解 指南。
关键漏洞
1. 算法混淆攻击
最危险的 JWT 漏洞。如果服务器在未经验证的情况下接受令牌中的 alg 头部,攻击者可以:
攻击方式:将算法从 RS256(非对称)更改为 HS256(对称),并使用服务器的公钥签署伪造的令牌:
// 攻击者伪造的令牌
header: { "alg": "HS256", "typ": "JWT" }
payload: { "sub": "admin", "role": "superadmin" }
// 使用服务器的公钥作为 HMAC 密钥进行签名
如果服务器使用公钥作为密钥来验证 HS256 令牌,伪造的令牌就会通过验证。
修复方案:始终显式指定预期的算法:
// 错误 - 接受令牌指定的任何算法
jwt.verify(token, key);
// 正确 - 强制指定算法
jwt.verify(token, key, { algorithms: ['RS256'] });
2. None 算法攻击
某些库接受 "alg": "none" — 一个没有签名的令牌:
// 没有签名的伪造令牌
header: { "alg": "none", "typ": "JWT" }
payload: { "sub": "admin", "role": "superadmin" }
signature: "" // 空
修复方案:在生产环境中永远不允许 none 算法。显式设置允许的算法白名单。
3. 弱签名密钥
基于 HMAC 的 JWT(HS256/HS384/HS512)的安全性取决于密钥的强度:
// 极差 - 几秒内即可暴力破解
secret = "password123"
// 较弱 - 容易受到字典攻击
secret = "my-jwt-secret"
// 强密钥 - 256+ 位随机数
secret = "a3f2b8c9d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0"
修复方案:HMAC 密钥至少使用 256 位加密随机数。更好的做法是使用非对称密钥(RS256、ES256),因为签名密钥无需共享。
4. 缺少过期时间
没有过期时间的令牌永远不会失效 — 被盗的令牌将授予永久访问权限:
// 错误 - 没有过期时间
{ "sub": "user123", "role": "admin" }
// 正确 - 短过期时间
{
"sub": "user123",
"role": "admin",
"exp": 1705312800,
"iat": 1705309200,
"nbf": 1705309200
}
最佳实践:访问令牌在 15-60 分钟内过期。使用刷新令牌(安全存储)来维持长会话。
5. 载荷中包含敏感数据
JWT 载荷是 Base64URL 编码的,而不是加密的。任何人都可以解码它们:
echo "eyJzdWIiOiIxMjMiLCJyb2xlIjoiYWRtaW4ifQ" | base64 -d
# {"sub":"123","role":"admin"}
永远不要包含:密码、API 密钥、信用卡号、个人数据(身份证号、医疗记录)或任何秘密信息。
6. 令牌存储(XSS 与 CSRF)
| 存储方式 | XSS 脆弱 | CSRF 脆弱 |
|---|---|---|
| localStorage | 是 | 否 |
| Cookie(无标志) | 是 | 是 |
| HttpOnly Cookie | 否 | 是 |
| HttpOnly + SameSite Cookie | 否 | 否 |
推荐方案:将 JWT 存储在 HttpOnly、Secure、SameSite=Strict Cookie 中。这可以防止 JavaScript 访问(XSS 防御)和跨站请求(CSRF 防御)。
Set-Cookie: token=eyJ...; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600
安全检查清单
- 显式指定算法 — 永远不要从令牌头部接受算法
- 拒绝
none算法 — 始终要求签名 - 使用强密钥 — HMAC 使用 256+ 位,RSA 使用 2048+ 位
- 设置短过期时间 — 访问令牌 15-60 分钟
- 验证所有声明 —
exp、iss、aud、nbf - 存储在 HttpOnly Cookie 中 — 不要使用 localStorage
- 定期轮换密钥 — 制定密钥轮换计划
- 永远不要在载荷中存储敏感数据
- 使用非对称密钥 用于分布式系统(RS256 或 ES256)
- 实现令牌撤销 — 黑名单或短过期时间 + 刷新令牌
非对称与对称签名
| 方面 | HS256(对称) | RS256(非对称) |
|---|---|---|
| 密钥 | 共享密钥 | 私钥/公钥对 |
| 谁可以签名 | 拥有密钥的任何人 | 仅私钥持有者 |
| 谁可以验证 | 拥有密钥的任何人 | 拥有公钥的任何人 |
| 密钥分发 | 必须安全共享密钥 | 仅共享公钥 |
| 性能 | 更快 | 更慢 |
| 最适合 | 单一服务 | 微服务、分布式系统 |
建议:新应用使用 ES256(ECDSA) — 它提供非对称安全性,性能接近 HMAC。
令牌撤销策略
JWT 在设计上是无状态的 — 没有内置的撤销方式。以下是可用策略:
- 短过期时间:如果令牌在 15 分钟内过期,被盗令牌的有效窗口有限
- 刷新令牌轮换:每次使用时签发新的刷新令牌;如果刷新令牌被使用两次,则撤销所有令牌
- 黑名单:存储已撤销令牌的 ID(jti 声明),并在每次请求时检查
- 令牌版本控制:在声明中包含版本号;用户注销时递增版本号
常见问题
我应该使用 JWT 还是会话 Cookie 进行认证?
对于传统 Web 应用程序,会话 Cookie 更简单、更安全 — 服务器控制会话生命周期,撤销是即时的。JWT 更适合无状态 API、微服务和移动应用,在这些场景中服务器端会话存储不切实际。如果选择 JWT,请实施本指南中的安全措施。
JWT 的理想过期时间是多长?
访问令牌:15-60 分钟。更短的时间更安全,但需要更频繁的刷新。刷新令牌:1-30 天,存储在 HttpOnly Cookie 中。一次性令牌(邮箱验证、密码重置):1-24 小时。
相关资源
- JWT 编码器/解码器 — 安全地检查和解码 JWT
- JWT 令牌详解 — 理解 JWT 结构和流程
- 密码熵值详解 — JWT 签名密钥的强度