3、令牌结构:JWT的Header、Payload、Signature详解
JWT 长什么样?说白了就是三段 Base64 编码的字符串,用点号连起来。我刚开始接触时也觉得这玩意儿挺简单,直到在生产环境踩了坑,才发现每一段都有门道。
一个典型的 JWT 长这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
三部分分别是 Header、Payload、Signature。咱们一个一个拆开看。
核心要点:JWT 的三段结构决定了它的安全边界。Header 管算法,Payload 管数据,Signature 管完整性。任何一段被篡改,签名校验都会失败。
3.1 Header:算法的声明
Header 是 JWT 的第一段。它告诉接收方:我用什么算法签的名,令牌是什么类型。
一个典型的 Header 长这样:
{
"alg": "HS256",
"typ": "JWT"
}
- alg:签名算法。常见的有 HS256(HMAC-SHA256)、RS256(RSA-SHA256)、ES256(ECDSA-SHA256)。
- typ:令牌类型。固定为 "JWT"。
嗯,这里要注意。alg 字段是攻击者的重点目标。我曾经在渗透测试中遇到过一种攻击,叫「alg=none」。攻击者把 alg 改成 "none",然后直接去掉签名部分。有些实现不严谨的库,居然就放行了。
避坑指南:服务端必须校验 alg 字段,拒绝 "none" 算法。我建议在代码里白名单化允许的算法列表,不在白名单里的直接拒绝。
另外,Header 里还可以放一些扩展字段。比如 jku(JWK Set URL)、jwk(公钥)、kid(密钥 ID)。这些在分布式场景下很有用,但也引入了额外的安全风险。我的建议是:除非你真的需要动态密钥轮换,否则别用 jku/jwk,直接用 kid 查本地缓存就够了。
3.2 Payload:数据的载体
Payload 是 JWT 的第二段。它存放实际的数据,也就是 Claims(声明)。
Claims 分三类:
| 类型 | 说明 | 示例 |
|---|---|---|
| Registered Claims | 预定义的标准化字段 | iss, sub, exp, iat, nbf, jti |
| Public Claims | 公开定义的字段,需在 IANA 注册 | name, email, role |
| Private Claims | 自定义字段,双方约定即可 | department, tenant_id |
一个典型的 Payload 示例:
{
"iss": "https://auth.example.com",
"sub": "user_123456",
"aud": "api.example.com",
"exp": 1735689600,
"iat": 1735603200,
"nbf": 1735603200,
"jti": "unique--id-001",
"name": "张三",
"role": "admin",
"tenant_id": "tenant_abc"
}
这里有几个字段,我个人觉得特别重要:
- exp(过期时间):必须校验。我见过不少系统没校验 exp,导致令牌永久有效。
- nbf(生效时间):防止令牌提前使用。适合预签发场景。
- jti(令牌唯一标识):用于防重放攻击。配合黑名单使用效果更好。
- iat(签发时间):审计用,也能辅助判断令牌是否被提前使用。
经验之谈:Payload 是 Base64 编码,不是加密。任何拿到令牌的人都能解码看到里面的内容。所以,不要把密码、信用卡号等敏感信息放在 Payload 里。我习惯的做法是:只放用户 ID 和角色这类非敏感标识,其他数据通过 API 查询。
3.3 Signature:完整性的保证
Signature 是 JWT 的第三段。它保证了令牌在传输过程中没有被篡改。
签名过程其实很简单:
signature = HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
接收方拿到令牌后,用同样的算法和密钥重新计算签名。如果计算结果和令牌里的签名一致,说明数据没被改过。
这里有个关键点:签名只保证完整性,不保证机密性。数据还是明文的,只是防止被篡改。如果你需要加密,得用 JWE(JSON Web Encryption),那是另一个话题了。
避坑指南:我曾经遇到过一个问题:服务端用 RS256(非对称算法)签名,但客户端用 HS256(对称算法)去验证。攻击者可以拿到公钥,然后用 HS256 伪造令牌。解决方案很简单:服务端必须校验 alg 字段是否和预期一致。
3.4 三者的协作关系
Header、Payload、Signature 三者缺一不可。它们的关系可以用一张图来表示:
你想想看,这三段的关系其实很像快递包裹:Header 是快递单上的服务类型(空运/陆运),Payload 是包裹里的东西,Signature 是封条。封条坏了,东西可能被换过,不能收。
3.5 常见误区与最佳实践
我在项目中见过不少 JWT 使用上的坑,这里列几个最常见的:
| 误区 | 后果 | 正确做法 |
|---|---|---|
| Payload 放敏感信息 | 信息泄露 | 只放非敏感标识,敏感数据走 API |
| 不校验 exp | 令牌永久有效 | 每次请求都校验 exp |
| 不校验 alg | alg=none 攻击 | 白名单化允许的算法 |
| 签名密钥硬编码 | 密钥泄露后无法快速轮换 | 使用密钥管理服务,支持动态轮换 |
| Payload 过大 | HTTP Header 超限 | 控制 Payload 在 1KB 以内 |
我的习惯:每次签发 JWT 时,我都会检查 Payload 里有没有不该出现的数据。另外,我会在 Header 里加一个 "kid" 字段,方便密钥轮换时快速定位用哪个密钥验证。这个小细节,在微服务架构里特别有用。
最后说一句:JWT 的结构设计其实很优雅。它把算法、数据、校验分得清清楚楚。但再好的设计,用错了地方也是白搭。理解这三段各自的责任边界,你就能避开 90% 的常见坑。