4. JWT Payload 详解:注册声明、公共声明与私有声明

JWT 的 Payload(载荷)是承载实际数据的核心部分,它是一个 JSON 对象,包含一组声明(Claims)。声明是关于实体(通常是用户)和附加数据的陈述。根据 JWT 标准(RFC 7519),声明分为三类:注册声明、公共声明和私有声明。理解这三类声明的用途、规范与安全约束,是设计健壮 JWT 接口契约的基础。

核心原则: Payload 仅进行 Base64Url 编码,并未加密。任何持有 JWT 的人都可以解码读取 Payload 内容。因此,绝对禁止在 Payload 中存放密码、密钥、完整身份证号等敏感信息。

4.1 注册声明(Registered Claims)

注册声明是一组预定义的、非强制但推荐使用的声明,旨在提供互操作性。它们由 IANA JSON Web Token Claims 注册表维护。以下是七个最核心的注册声明:

声明名 全称 类型 描述与约束
iss Issuer 字符串或 URI 签发者。标识签发 JWT 的主体。通常为应用的唯一标识或 URL。接收方应验证 iss 是否与预期签发者匹配。
sub Subject 字符串或 URI 主题。标识 JWT 所面向的用户或实体。通常是用户的唯一 ID(如 user_12345)。在同一个 iss 范围内,sub 应全局唯一。
aud Audience 字符串或字符串数组 接收方。标识 JWT 的预期接收者。接收方必须验证自己是否在 aud 列表中,否则应拒绝该 JWT。常用于区分微服务。
exp Expiration Time 数字(NumericDate) 过期时间。JWT 在此时间之后不得被接受处理。这是一个强制验证字段,建议设置较短有效期(如 15-60 分钟)。
nbf Not Before 数字(NumericDate) 生效时间。JWT 在此时间之前不得被接受处理。用于延迟签发场景,避免提前使用。
iat Issued At 数字(NumericDate) 签发时间。标识 JWT 的签发时刻。可用于计算 token 年龄或判断是否在允许的时钟偏差范围内。
jti JWT ID 字符串 唯一标识符。用于防止 JWT 重放攻击。服务端需维护已使用的 jti 列表(黑名单),确保每个 jti 只被使用一次。
安全警示:
  • exp 必须验证: 服务端必须检查当前时间是否已超过 exp,否则 JWT 将永久有效。
  • 时钟偏差(Clock Skew): 验证 exp 和 nbf 时,建议允许最多 30-60 秒的时钟偏差,以容忍分布式系统间的时间不同步。
  • jti 与重放攻击: 对于高安全场景(如支付、转账),必须使用 jti 并配合服务端存储(如 Redis)实现一次性 token。

注册声明验证流程图

JWT 注册声明验证流程 JWT 注册声明验证流程 接收 JWT 验证 iss(签发者) 验证 aud(接收方) 验证 exp / nbf(时间) 验证 jti(防重放) 验证要点 • iss 必须与预期签发者一致 • aud 必须包含当前服务 • 当前时间必须在 nbf 之后 • 当前时间必须在 exp 之前

4.2 公共声明(Public Claims)

公共声明是由使用者自行定义的声明,但为了避免与注册声明或其他公共声明冲突,必须遵循以下两种方式之一:

  • 方式一:使用 IANA 注册 — 在 IANA JSON Web Token Claims 注册表 中注册你的声明名称,确保全球唯一性。
  • 方式二:使用 Collision-Resistant Name(防冲突命名) — 将声明名定义为包含命名空间的 URI 或具有足够复杂度的字符串,例如:https://api.example.com/claims/role 或 com.example.role。
最佳实践: 对于企业内部微服务,推荐使用 反向域名 + 声明名 的格式,如 com.yourcompany.users.role。这既保证了唯一性,又便于团队理解和维护。

公共声明示例

{
  "iss": "https://auth.example.com",
  "sub": "user_98765",
  "aud": "https://api.example.com",
  "exp": 1735689600,
  "iat": 1735686000,
  // 以下是公共声明(使用防冲突命名)
  "https://api.example.com/claims/role": "admin",
  "com.example.department": "engineering",
  "com.example.permissions": ["read:users", "write:projects"]
}

4.3 私有声明(Private Claims)

私有声明是通信双方(签发者和接收者)之间私下约定的声明,不进行注册,也不使用防冲突命名。它们仅在特定的、受控的上下文中有效。

  • 适用场景: 单一应用内部、前后端之间、或两个紧密耦合的微服务之间。
  • 风险: 如果服务被外部调用或与其他系统集成,私有声明可能发生命名冲突,导致意外覆盖或解析错误。
  • 建议: 除非是绝对受控的内部环境,否则优先使用公共声明(带命名空间)替代私有声明。

私有声明示例

{
  "iss": "auth-service",
  "sub": "user_123",
  "exp": 1735689600,
  // 以下是私有声明(仅内部约定)
  "role": "editor",
  "dept": "marketing",
  "is_temp": false
}
避坑指南: 私有声明 role 和 dept 是高度通用的名称。如果未来某个中间件或第三方库也使用了同名的注册声明或公共声明,你的 JWT 将产生不可预料的解析错误。因此,新项目应默认使用公共声明命名方式。

4.4 三类声明对比总结

特性 注册声明 公共声明 私有声明
定义者 RFC 7519 标准 使用者(需注册或防冲突命名) 通信双方私下约定
命名唯一性 由标准保证 通过注册或命名空间保证 不保证,存在冲突风险
互操作性 高(跨系统通用) 中(需双方理解命名约定) 低(仅限内部约定)
推荐场景 所有 JWT 都应包含 跨服务、跨系统共享数据 严格内部、快速原型
示例 iss, sub, exp com.example.role role, dept

4.5 Payload 安全设计清单

  1. 最小化原则: Payload 只存放必要的数据,避免冗余信息增加 token 体积。
  2. 禁止敏感数据: 密码、密钥、完整身份证号、信用卡号等绝对不可放入 Payload。
  3. 使用短有效期: exp 建议设置为 15-60 分钟,降低 token 泄露风险。
  4. 验证所有注册声明: 服务端必须验证 iss、aud、exp、nbf(如果存在)。
  5. 防重放: 关键操作必须使用 jti 并维护已用 ID 列表。
  6. 命名规范: 优先使用公共声明命名方式,避免私有声明带来的冲突隐患。
实战建议: 在接口契约文档中,明确列出所有自定义声明的名称、类型、含义和示例值。对于公共声明,标注其命名空间来源。这能有效减少团队协作中的理解偏差。