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 中,不产生转义问题。解码时反向替换即可。
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 等非对称算法,签发方用私钥签名,校验方用公钥验签。这种模式适合多服务场景:认证中心持有私钥,各资源服务只需公钥即可独立校验,无需共享对称密钥,降低了密钥泄露的爆炸半径。
一个可对照的声明示例
下面是一个包含三类声明的 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 中的声明。理解三段结构与三类声明的语义边界,是构建可靠令牌治理策略的基础。