JWT 完全指南:原理、5 大安全陷阱与生产最佳实践
每个写过登录系统的人都见过 JWT——那串看起来像随机字符的 eyJhbGci...xxxx.yyyy.zzzz。它的卖点很诱人:无状态认证,服务端不存 Session,水平扩容随便加。
但 JWT 也是公认的陷阱密集型技术。OWASP 把 JWT 相关问题列在常见漏洞清单的显眼位置,2015 到现在大大小小的算法漏洞、签名绕过、密钥泄露事故层出不穷——很多团队直到上线才发现:用 JWT 写得安全比写出来难得多。
这篇文章把 JWT 真正该知道的事一次讲清:结构、算法、5 大安全陷阱、Refresh Token 怎么设计、以及最重要的——什么时候根本不该用 JWT。
JWT 是什么:三段式结构拆解
一个 JWT 长这样:
用点号分成三段:Header.Payload.Signature。
Header(头部)
Base64URL 解码后是 JSON:
alg:签名算法typ:固定JWT- 可选
kid:密钥 ID,多密钥轮换时定位用哪把
Payload(载荷)
包含一组 Claim(声明):
标准 Claim(建议用):
| 字段 | 含义 | 必要性 |
|---|---|---|
iss | Issuer,签发方 | 推荐 |
sub | Subject,用户唯一标识 | 推荐 |
aud | Audience,目标接收方 | 推荐 |
exp | Expiration,过期时间戳 | 必须 |
iat | Issued At,签发时间 | 推荐 |
nbf | Not Before,生效时间 | 可选 |
jti | JWT ID,唯一 ID(用于撤销) | 可选 |
Payload 是 Base64URL 编码,不是加密。任何能拿到 JWT 字符串的人都能解码看到全部内容。绝对不要把密码、信用卡号、私钥这类敏感数据塞进 Payload。
Signature(签名)
签名计算公式:
签名保证两件事:
- 完整性:Payload 被篡改后,签名验不通过
- 真实性:只有持有 secret 的人才能签出有效 Token
算法选型:HS256 vs RS256 vs ES256
| 算法 | 类型 | 密钥 | 适用场景 |
|---|---|---|---|
| HS256 | HMAC + SHA-256 | 单一对称密钥 | 单服务、内部使用 |
| HS384 / HS512 | HMAC 高强度变体 | 同上 | 同上 |
| RS256 | RSA + SHA-256 | 私钥签 + 公钥验 | 多服务、公开验证 |
| ES256 | ECDSA + SHA-256 | 椭圆曲线私钥/公钥 | 同 RS256,性能更高、Token 更短 |
| EdDSA | Ed25519 | 新一代椭圆曲线 | 现代推荐 |
| none | 无签名 | 无 | 永远不要用(见下文) |
怎么选
- 单服务内部:HS256 够用,部署最简单
- 微服务体系/第三方接入:RS256 或 ES256——签发方用私钥,所有验证方用公钥,密钥分发安全得多
- 新项目优先考虑:EdDSA / ES256(性能好,Token 更小)
5 大安全陷阱(生产真出过事的)
陷阱 1:alg=none 绕过
JWT 规范里有一个奇葩算法 none,表示"无签名"。早期一些库默认会接受这种 Token——意味着攻击者可以构造:
加上任意 Payload,不带签名也能通过验证。
修复:
所有主流库现在都默认禁止 none,但接手老代码或者用冷门库时务必检查。
陷阱 2:算法降级(HS256 vs RS256)
服务端用 RS256,公钥公开。攻击者把 Header 改成:
然后用公钥当作 HMAC 的 secret 去签名。如果服务端代码长这样:
攻击者就能用公钥(公开的)伪造任意 Token。
修复:
绝对不要根据 Token 自己声明的算法去验证。
陷阱 3:密钥太弱
HS256 用的 secret 如果是 secret123 或 mysecret,可以离线暴力破解:
实测中等密码 1 分钟搞定。
修复:
-
HS256/HS384/HS512 用 至少 256 位(32 字节)随机密钥:
-
不要从代码、配置文件硬编码——从环境变量或密钥管理服务(KMS、Vault)读取
-
不要复用密码、API Key 这类"看起来像 secret"的字符串
陷阱 4:过期时间不验/无过期
更糟糕的:签发时根本没设 exp。Token 永久有效,一次泄露毁所有。
修复:
- 必须设 exp,建议短期(15 分钟 - 2 小时)
- 用 Refresh Token 机制延长会话(见下文)
- 验证时强制
verify_exp=True(库默认行为,别手动关)
陷阱 5:JWT 无法撤销
JWT 的设计就是无状态——只要签名有效、没过期,服务端就承认。问题是:
- 用户改密码 → 旧 Token 还能用
- 用户登出 → 旧 Token 还能用
- 怀疑账号被盗 → 旧 Token 还能用
修复方案(按推荐度):
- 短 exp + Refresh Token:访问 Token 15 分钟过期,影响窗口可控
- 黑名单(Redis 存 jti):登出时把 jti 写黑名单,验证时查一次——但这就破坏了"无状态"的初衷
- 改密码后改密钥/Token 版本号:Payload 加
tokenVersion,用户改密码时递增;验证时对比当前版本——只对"全部登出"场景有效
Refresh Token 设计模式
业内标准方案:Access Token + Refresh Token 双令牌。
关键设计点
1. Access Token 用 JWT,Refresh Token 用随机字符串
Access Token 短命+无状态,性能优先;Refresh Token 长命+可撤销,安全优先。
2. Refresh Token 必须存数据库
存到 Redis 或 DB 表,记录:
| 字段 | 用途 |
|---|---|
| token_hash | Token 的 SHA-256 哈希(不存明文) |
| user_id | 关联用户 |
| device_info | 设备/IP/UA(便于审计) |
| expires_at | 过期时间 |
| revoked | 是否撤销 |
3. Refresh Token Rotation(轮换)
每次用 Refresh Token 换新 Access Token 时,同时签发新的 Refresh Token 并作废旧的。这样即使 Refresh Token 泄露,攻击者用一次后真用户再用就会触发"Token 已使用"告警。
4. Refresh Token 存放位置
| 存放位置 | 优点 | 缺点 |
|---|---|---|
| HttpOnly Cookie | 抗 XSS | 需要处理 CSRF |
| localStorage | 实现简单 | 易被 XSS 偷 |
| Native App 安全存储 | iOS Keychain / Android Keystore | 仅 App |
推荐:Web 端用 HttpOnly + Secure + SameSite=Strict 的 Cookie,App 端用平台安全存储。
实战代码示例
Node.js (jsonwebtoken)
Python (pyjwt)
调试技巧
解码看 Payload
不要肉眼数 Base64——用工具:
-
站内 JWT Decoder 一键解码 Header + Payload
-
jq配合base64:
在线验证签名
JWT Decoder 工具也支持输入 secret 验证签名,调试时极其方便。生产 Token 别贴到第三方在线网站。
跟踪 Token 全生命周期
加日志记录关键事件:
出问题时按 jti 串起来一目了然。
什么时候不该用 JWT
JWT 不是银弹。下面这些场景请用传统 Session:
| 场景 | 为什么 Session 更好 |
|---|---|
| 单体应用、单数据中心 | Session 简单可靠,撤销直接删 |
| 用户态频繁变更(权限、订阅状态) | JWT 缓存数据,变更后不及时 |
| 严格审计/撤销需求(金融、医疗) | Session 服务端可控,JWT 难撤销 |
| Token 要塞大量用户数据 | JWT 越塞越大,每次请求都带 |
| 极致性能场景(高并发 API) | HMAC 验证仍有开销,Session 查 Redis 更快 |
JWT 真正适合的场景:
- 跨服务/微服务调用(无中心鉴权服务)
- 短期一次性令牌(邮件验证、密码重置链接)
- OAuth / OIDC 体系下的 ID Token
- 移动端长会话(配合 Refresh Token)
总结
JWT 用得对是优雅的,用得不对是定时炸弹。把这几条钉在脑子里:
- 算法必须白名单,禁用 none,禁用动态 alg
- 密钥至少 256 位随机,从环境变量/KMS 读
- exp 必须设,短期为主(15 分钟级别)
- 敏感数据不进 Payload,它只是 Base64 编码不是加密
- 配合 Refresh Token 做长会话,配合数据库做撤销
- 不需要无状态时用 Session,别为了"看起来现代"硬上 JWT
写完代码用站内的 JWT Decoder 解一遍看 Header/Payload,用 Hash 生成器 生成强随机密钥——能消灭你 80% 的 JWT 配置错误。