三段结构
JWT 由 header.payload.signature 三段组成,用点号连接。前两段是 Base64Url 编码的 JSON——只是编码,不是加密:任何人复制出去解码就能看到内容。第三段是签名,用来验证前两段没有被改动。
实际长什么样
// header(解码后)
{ "alg": "HS256", "typ": "JWT" }
// payload(解码后)—— 这些字段任何人可见
{ "sub": "user_1024", "role": "admin", "iat": 1758000000, "exp": 1758003600 }
// 最终令牌
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyXzEwMjQifQ.3f9c...(签名)
三个致命误区
- 把敏感信息放进载荷:手机号、身份证号、内部权限细节放进 payload,等于明文公开;JWE 才是加密方案;
- 只在前端解码就信任身份:前端解出的
role: admin毫无意义,服务端必须验签(并校验iss、aud、exp); - 不校验算法与有效期:忽略
alg会遭遇alg: none或算法混淆攻击;不校验exp则过期令牌永久有效。
工程上会遇到的问题
- 无法主动失效:JWT 自包含,签发后难以撤销;常见做法是短有效期(5–15 分钟)+ refresh token,必要时维护黑名单;
- 体积膨胀:把大量权限塞进 payload,会让每个请求头都变大,注意 4KB 级别的常见上限;
- 时钟偏移:客户端与服务器时间不一致会导致“刚签发就过期”,校验时可留几十秒容差。
常见问题
JWT 和 Session 怎么选?无状态、多服务横向扩展的场景适合 JWT;需要即时撤销、权限频繁变化则用服务端 Session 更简单。签名用什么算法?单服务共享密钥用 HS256,多方验签用 RS256/ES256(公钥验签、私钥签发)。
JWT 与服务端 Session 的取舍
| 维度 | JWT | 服务端 Session |
|---|---|---|
| 状态 | 无状态,服务端不保存会话 | 服务端保存会话 |
| 撤销 | 困难,需要黑名单或短有效期 | 立即可撤销 |
| 横向扩展 | 简单,无需共享存储 | 需要共享存储或粘性会话 |
| 请求开销 | 令牌较大,每次请求携带 | 只携带一个会话标识 |
实践中最常见的是折中方案:短期访问令牌(JWT) + 可撤销的刷新令牌(服务端存储),既保留无状态的扩展优势,又能在必要时立即失效。
实战案例:三个真实的安全事故
- “把 alg 改成 none 就能绕过校验”:早期库若信任头部里的 alg,攻击者可伪造任意令牌。修复:服务端固定允许的算法,绝不信任头部。
- “HS256 的密钥用了示例默认值”:很多人拿文档里的 secret 直接上线,等于没有签名。修复:使用足够长、随机且不明文入库的密钥,并定期轮换。
- “令牌无法撤销”:JWT 是无状态的,签发后在过期前一直有效。修复:缩短有效期 + 刷新令牌,或维护服务端黑名单 / 版本号。
常见问题(FAQ)
JWT 是加密的吗?默认(JWS)不是,只签名不加密,负载可被任何人 Base64 解码读取,切勿放敏感信息。能放权限吗?可以,但权限变更后旧令牌仍带旧权限,需靠短有效期或版本号兜底。存在哪里?localStorage 易受 XSS,HttpOnly Cookie 更稳但要防 CSRF;按威胁模型权衡。一定要用 JWT 吗?不一定,服务端会话在很多场景更简单可控。
动手试试:Base64 编解码、哈希计算
算法混淆与其它攻击面
- 算法混淆:若校验方接受令牌头部指定的算法,攻击者可把
RS256改成none或用公钥当 HMAC 密钥,服务端必须固定允许的算法白名单; - 拒绝
none:任何实现都不应接受无签名令牌; - 校验全部声明:
iss、aud、exp、nbf都要校验,只验签名等于只看“没被改过”; - 密钥要够强:HS256 的密钥若过短可被离线爆破,应使用足够长的随机密钥;
- 别放敏感数据:载荷只是 Base64 编码而非加密,任何人都能读取。
与 Session 的取舍
JWT 的优势是无状态与跨服务传递,代价是难以即时吊销。折中做法是使用短期访问令牌(5–15 分钟)配长效刷新令牌,刷新令牌存服务端并支持吊销;这样既减少查库,又保留“立即下线”的能力。