一、alg: none 与算法混淆
JWT 头部声明了签名算法。如果服务端信任这个字段,就可能被攻击:
- 攻击者把
alg改成none,去掉签名段。实现不严谨的库会直接放行。 - 攻击者把
HS256改成RS256,然后用公开的公钥当 HMAC 密钥重算签名。服务端如果按头部声明切算法,就会用公钥去验 HMAC,正好通过。
防护只有一条:服务端硬编码允许的算法白名单,忽略客户端声明的 alg。
jwt.verify(token, key, { algorithms: ['RS256'] }); // 必须显式指定
二、密钥太弱
HS256 的密钥就是一个字符串。用 secret、123456、项目名当密钥,等于没有签名——攻击者离线暴力破解后可以伪造任意 token。
建议:至少 32 字节的密码学随机值,用 crypto.randomBytes(32).toString('base64url') 生成,通过环境变量或 KMS 注入,定期轮换(轮换时用 kid 头部支持多密钥并存)。
三、Payload 是明文
Base64URL 只是编码。任何人拿到 token 都能读出里面的字段,包括用户 ID、角色、手机号。所以:
- 不要放密码、身份证号、完整手机号、内部接口地址。
- 不要放「不想让用户知道自己有这个权限」的信息。
- 不要把整个用户对象塞进去——token 会变长,每个请求都要传。
一个常见错误是把权限列表放 Payload 后直接信任它。如果角色变了,旧 token 在过期前仍然带着旧权限;反过来,如果权限判断依赖 payload,就要接受「权限变更延迟到 token 过期」这个事实。
四、无法主动注销
JWT 是自包含的,服务端不存状态,所以签发后无法撤回。用户点「退出登录」、改了密码、账号被封,旧 token 在 exp 之前依然有效。
几种折中方案:
| 方案 | 代价 |
|---|---|
| 短有效期(15 分钟)+ Refresh Token | 推荐,泄露窗口小 |
| 服务端维护黑名单(jti 或用户级 tokenVersion) | 引入状态,但可控 |
| 改密码时更新用户 tokenVersion,校验时比对 | 只解决「改密码后失效」 |
| 纯前端删除 token | 只是「看不见」,不防重放 |
生产环境通常是「短 access token + 长 refresh token + refresh 时校验用户状态」的组合。
五、存在哪里
| 位置 | XSS 风险 | CSRF 风险 | 说明 |
|---|---|---|---|
| localStorage | 高(脚本可读) | 无 | 方便,但一旦有 XSS 就全丢 |
| 内存变量 | 低 | 无 | 刷新页面即失效,需配合 refresh |
| HttpOnly Cookie | 低 | 高(需 SameSite + CSRF token) | 推荐,但要防 CSRF |
没有完美选项。比较稳的组合是:refresh token 放 HttpOnly + Secure + SameSite=Lax 的 Cookie,access token 只放内存,请求时放 Authorization 头。
六、还有两个细节
- 必须校验
exp、nbf、iss、aud。很多库默认只验签名,不验 issuer,导致「另一个系统的合法 token」也能通过。 - 必须走 HTTPS。token 在 header 里明文传输,中间人拿到就能直接冒充。
调试时用 JWT 解析工具 看三段内容和过期时间(本地解码,不上传);需要复核签名计算时,用 HMAC 生成 对 header.payload 做一次,再与签名段比对。
小结
算法白名单、密钥够长、Payload 不放敏感信息、短有效期 + Refresh、HttpOnly 存 refresh。JWT 的便利来自无状态,风险也全部来自无状态。