2026年JWT安全最佳实践
避免最常见的JWT漏洞: 算法混淆、弱密钥和重放。
JWT在数百万个系统中承载认证状态,而同样几个实现错误持续制造出完全的账户接管漏洞。JWT最佳实践文档RFC 8725之所以存在,正是因为基础规范(RFC 7519)过于宽松,无法天真地直接使用。下面是实际会出问题的地方,以及每种失败的预防方法。
算法混淆仍是头号漏洞
令牌头部自行声明其算法——信任这一声明的验证器等于把钥匙交给攻击者。两种经典攻击:
- alg none。 规范允许
"alg": "none"的无签名令牌。接受它们的库会把任何伪造的载荷当作已验证。这个攻击已有十多年历史,却仍在旧库的新封装中反复出现。 - RS256降级为HS256。 验证RS256令牌的服务通常持有RSA公钥。如果攻击者构造一个
"alg": "HS256"的令牌,并用该公钥作为HMAC密钥来签名,那么根据头部alg进行分发的库会成功验证它——公钥在定义上就是公开的。
修复只有一行,而且不是可选项:
jwt.verify(token, key, { algorithms: ['RS256'] });
在服务端固定精确的算法列表。永远不要让令牌自己选择。
2026年的算法选择
- HS256 — 对称HMAC。当签发者和验证者是同一服务时适用。密钥必须是随机的256位值:
openssl rand -base64 32。人类想出的口令短语会把令牌伪造降级为针对单个HMAC的离线字典攻击。 - RS256 — RSA签名。支持广泛,验证者只需要公钥,但签名较大且RSA密钥生成慢。
- ES256 / EdDSA — 椭圆曲线。令牌更小、验证更快;EdDSA(Ed25519)还消除了曾多次搞垮ECDSA部署的nonce重用陷阱。新的分布式系统优先选择它们。
通过JWKS端点分发公钥,轮换时先发布新密钥、再用它签名。有两个头部字段值得怀疑:jku 和 x5u 允许令牌把验证密钥指向任意URL。RFC 8725对此毫不含糊——除非URL在严格的白名单上,否则忽略它们。同样,把 kid 当作不透明的查找标识符;把它插入文件路径或SQL查询的实现,两种方式都曾被利用过。
验证每个声明,而不只是签名
密码学上有效的令牌仍可能是错误的令牌:
const payload = jwt.verify(token, key, {
algorithms: ['ES256'],
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
clockTolerance: 5
});
- iss — 跳过它会放行来自其他(可能是攻击者注册的)租户的令牌。
- aud — 跳过它会让为一个API签发的令牌在另一个API上重放。这是微服务集群中经典的跨服务混淆代理漏洞。
- exp / nbf — 用小的时钟容差(秒级,而非分钟级)强制执行。
- typ — RFC 9068为访问令牌定义了
at+jwt;检查它可以防止ID令牌或刷新令牌被当作访问令牌接受。
生命周期、存储与吊销是同一个设计
这三个决策只有放在一起才有意义:
- 访问令牌: 5到15分钟,只保存在内存中。 永远不要用localStorage——任何XSS都能同步读取它,httpOnly标志在那里帮不了你。
- 刷新令牌: httpOnly、Secure、SameSite=Strict的cookie,并配合轮换:每次刷新签发新令牌并作废旧令牌。如果一个已被轮换掉的令牌再次出现,吊销整个会话——这种重用就是被盗的签名。
- 吊销: 无状态JWT无法吊销,因此要让生命周期短到暴露窗口可以接受,并维护一个以
jti声明为键的服务端拒绝列表,用于登出和入侵响应。如果你的产品需要随处即时、可靠的吊销,那正是应该改用会话存储加不透明令牌的信号——这是完全体面的架构。
载荷卫生
载荷是base64url编码,不是加密。任何拿到令牌的人一次解码调用就能读到所有声明。不要放入PII、你视为机密的角色信息和内部标识符,并保持令牌精简——它随每个请求传输,cookie里4KB的声明可能超出代理和CDN的头部限制。
调试清单
- 部署后签名无效: 密钥轮换时JWKS没有重叠期。
- 本地有效、生产环境拒绝: 时钟偏移,或代理剥离了Authorization头。
- 每到整点出现间歇性401: 在漂移的虚拟机时钟上验证exp却没有设置clockTolerance。
- 能解码但verify失败: 你在该调用verify的地方调用了decode——decode不执行任何密码学检查。
JWS、JWE,以及JWT实际命名的是什么
严格来说,JWT命名的是声明格式;容器要么是JWS(已签名、可读),要么是JWE(已加密)。现实中几乎所有的「JWT」都是JWS,这正是载荷中没有任何内容是机密的原因。如果某个声明必须对客户端隐藏——内部用户标记、风险评分——要么把它留在令牌背后的服务端,要么使用JWE并接受随之而来的密钥管理复杂度。为了隐藏本可以干脆不发送的数据而加密,通常是错误的取舍。
用 sdk.is/jwt-decoder 的JWT解码器安全地检查头部和声明——解码是检查手段,永远不是认证。