2. JWT结构深度解析

JWT(JSON Web Token)在网络上传输时,本质上是一个由两个英文句点分隔的三段式字符串。理解这三段分别是什么、如何编码、各自承载什么语义,是后续讨论签名校验、过期治理、声明冲突与安全边界的前提。本节先拆解编码与组成,再深入三类声明的语义与使用边界。

三段式结构:Base64URL 编码的拼接

一个典型的 JWT 形如:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNzAwMDAwMDAwfQ
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

三段依次是 Header(头部)、Payload(载荷)、Signature(签名),用 . 连接。前两段是 JSON 对象经过 Base64URL 编码后的结果,第三段是对前两段拼接串做签名算法运算后的结果。这里必须强调:Base64URL 是编码而非加密,任何人拿到 JWT 都能把前两段解码还原成明文 JSON。因此 Payload 中绝不能放置密码、身份证号、密钥等敏感信息。

Base64URL 与标准 Base64 的差异在于:把 + 换成 -,把 / 换成 _,并去掉末尾的 = 填充。这样做的目的是让 JWT 能安全地放进 URL、HTTP 头或 Cookie 中,不产生转义问题。解码时反向替换即可。

JWT 三段结构与编码关系 Header {"alg":"HS256", "typ":"JWT"} 算法与类型元数据 Payload {"sub":"1234567890", "name":"Alice","iat":...} 声明集合(Claims) Signature HMACSHA256( base64url(h)+"."+ base64url(p), secret) . . eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiIxMjM0In0 . SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c Base64URL 编码段 · Base64URL 编码段 · 签名段 编码 ≠ 加密 前两段可被任意解码还原为明文 JSON Payload 中禁止存放密码、密钥等敏感数据 签名只保证完整性,不提供机密性 签名计算输入 signingInput = base64url(header) + "." + base64url(payload)

Header:算法与类型的元数据

Header 是一个 JSON 对象,最关键的字段是 alg,它声明签名与校验所用的算法,例如 HS256(HMAC-SHA256 对称算法)、RS256(RSA-SHA256 非对称算法)、ES256(ECDSA-SHA256)。另一个常见字段是 typ,通常为 JWT,用于标识令牌类型。当使用 JWK 或需要指定密钥时,还会出现 kid(Key ID),帮助校验方在密钥集合中定位正确的密钥。

Header 的安全意义在于:alg 是攻击面之一。经典的 alg:none 攻击就是伪造一个 alg 为 none 的 Header,诱导服务端跳过签名校验。因此服务端必须显式白名单允许的算法,绝不能信任 Header 中声明的 alg 来决定校验策略。

Payload:三类声明的语义与边界

Payload 是 JWT 的核心,它由一组“声明”(Claims)组成。声明是关于实体(通常是用户)和其他数据的陈述。按 JWT 规范(RFC 7519),声明分为三类:Registered Claims、Public Claims、Private Claims。

Registered Claims(注册声明)是规范预定义的一组声明,它们不是强制使用,但一旦使用就应遵循规范语义。常见的有:

  • iss(Issuer):签发者,标识谁签发了这个令牌。
  • sub(Subject):主体,通常是用户唯一标识。
  • aud(Audience):受众,标识令牌的预期接收方。
  • exp(Expiration Time):过期时间,Unix 时间戳,校验方必须检查。
  • nbf(Not Before):生效时间,早于此时间令牌不可用。
  • iat(Issued At):签发时间,可用于判断令牌年龄。
  • jti(JWT ID):令牌唯一标识,常用于防重放和黑名单。

这些声明是生命周期治理的关键抓手:exp 决定令牌何时失效,nbf 控制生效窗口,jti 支持撤销与重放检测,aud 与 iss 用于跨服务信任边界校验。

Public Claims(公共声明)是使用方自行定义、但为避免冲突而应在 IANA JSON Web Token Registry 中注册,或使用防冲突命名空间(如 URI)的声明。例如 https://example.com/roles。它们适合跨组织、跨系统共享的语义,边界在于:一旦公开,就应保持稳定,不能随意改变含义。

Private Claims(私有声明)是签发方与消费方私下约定使用的声明,如 tenant_id、dept_code。它们不对外承诺语义,仅限内部系统使用。边界在于:私有声明不能与注册声明或公共声明重名,否则会造成语义冲突;同时它们同样可被解码,不能承载敏感信息。

三类声明的选择原则:能用注册声明表达的就用注册声明;需要跨系统共享且语义稳定的用公共声明;仅内部使用、不对外承诺的用私有声明。命名冲突是常见事故来源,建议私有声明加业务前缀,如 app_role。

Signature:完整性与来源认证

Signature 的计算方式取决于 alg。以 HS256 为例,签名输入是 base64url(header) + "." + base64url(payload),然后用密钥做 HMAC-SHA256 运算,结果再做 Base64URL 编码,得到第三段。校验方用同样的密钥和算法重新计算,比对是否一致。

这里有一个容易被忽视的细节:签名覆盖的是编码后的字符串,而不是解码后的 JSON。这意味着 JSON 字段顺序、空格、换行都会影响签名结果。因此服务端在重新签发或校验时,必须使用原始编码串,不能先解码再重新序列化,否则会因序列化差异导致校验失败。

对于 RS256、ES256 等非对称算法,签发方用私钥签名,校验方用公钥验签。这种模式适合多服务场景:认证中心持有私钥,各资源服务只需公钥即可独立校验,无需共享对称密钥,降低了密钥泄露的爆炸半径。

常见误区:认为 JWT 是加密的。实际上 JWT 默认只做签名,Payload 是明文可读的。若确实需要机密性,应使用 JWE(JSON Web Encryption),而不是在 JWS 的 Payload 里塞敏感数据。

一个可对照的声明示例

下面是一个包含三类声明的 Payload 示例,用于对照理解边界:

{
  "iss": "https://auth.example.com",   // Registered
  "sub": "user_1024",                  // Registered
  "aud": "https://api.example.com",    // Registered
  "exp": 1700003600,                   // Registered
  "iat": 1700000000,                   // Registered
  "jti": "a1b2c3d4-e5f6-7890",         // Registered
  "https://example.com/roles": ["admin"], // Public
  "tenant_id": "t_88"                  // Private
}

校验方拿到这个令牌后,应依次检查:签名是否有效、exp 是否未过期、nbf 是否已生效、iss 与 aud 是否匹配预期、jti 是否在黑名单中。只有这些检查全部通过,才应信任 Payload 中的声明。理解三段结构与三类声明的语义边界,是构建可靠令牌治理策略的基础。