4. JWT 生命周期管理:签发、过期、生效时间与唯一标识
JWT 并非“签发即永久有效”的令牌。在分布式鉴权体系中,必须精确控制令牌的生效窗口与唯一性,否则会引入重放攻击、令牌泄露后无法撤销等严重风险。本章围绕 JWT 载荷中的四个核心字段——iat(签发时间)、exp(过期时间)、nbf(生效时间)、jti(唯一标识)——展开实战级解析。
4.1 字段总览与语义对比
下表列出四个字段的声明位置、数据类型与核心语义,后续各节逐一深入。
| 字段 | 全称 | 数据类型 | 语义 | 是否必选(RFC 7519) |
|---|---|---|---|---|
iat |
Issued At | NumericDate | 令牌签发的时间戳 | 否(强烈建议) |
exp |
Expiration Time | NumericDate | 令牌的绝对过期时间点 | 否(强烈建议) |
nbf |
Not Before | NumericDate | 令牌最早可被接受的时间点 | 否 |
jti |
JWT ID | String | 令牌的唯一标识符,防重放 | 否 |
时间戳单位:所有 NumericDate 字段均使用 Unix 时间戳(自 1970-01-01 00:00:00 UTC 起的秒数)。服务端校验时需注意时钟同步,建议所有节点使用 NTP 对齐,容忍误差不超过 30 秒。
4.2 iat(签发时间)—— 令牌的出生证明
iat 记录令牌的签发时刻。虽然 RFC 7519 未强制要求,但在以下场景中不可或缺:
- 令牌老化分析:通过
iat与exp的差值计算令牌剩余有效期,用于客户端提前刷新。 - 黑名单时间窗口:当令牌被撤销时,仅需拒绝
iat早于撤销时间的令牌,无需存储所有已签发令牌。 - 审计溯源:日志中记录
iat可定位签发服务实例与签发时刻。
// 签发时设置 iat(Go 示例)
claims := jwt.MapClaims{
"iat": time.Now().Unix(),
"exp": time.Now().Add(1 * time.Hour).Unix(),
"sub": "user_1001",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signedToken, _ := token.SignedString([]byte("secret"))
4.3 exp(过期时间)—— 令牌的生命终点
exp 是 JWT 生命周期中最关键的字段。服务端在验证 JWT 时,必须检查当前时间是否已超过 exp,超过则直接拒绝。
常见陷阱:不要依赖客户端删除过期令牌。攻击者可能持有已签发的令牌副本,服务端必须做
exp 校验。另外,exp 是绝对时间,不要设置为“相对过期时长”,必须转换为 Unix 时间戳。
过期策略对比:
| 策略 | 典型有效期 | 适用场景 | 风险 |
|---|---|---|---|
| 短过期 + 刷新令牌 | 15 分钟 ~ 1 小时 | Web 应用、移动端 API | 需额外管理 refresh token |
| 长过期 + 黑名单 | 24 小时 ~ 7 天 | 服务间通信、IoT 设备 | 黑名单存储压力大 |
| 无过期(不推荐) | 永久 | 仅限内部测试 | 泄露后无法撤销 |
4.4 nbf(生效时间)—— 令牌的“预热期”
nbf 定义令牌在哪个时间点之后才被接受。常用于以下场景:
- 定时任务授权:签发一个未来才生效的令牌,用于预授权某个定时操作。
- 分布式时钟补偿:签发服务与验证服务之间存在已知时钟偏差时,设置
nbf = iat + 5s避免因时钟不同步导致令牌被提前拒绝。 - 条件激活:用户付费后,令牌在付费生效时间点才可用。
// 设置 nbf 为签发后 10 秒生效
claims["nbf"] = time.Now().Add(10 * time.Second).Unix()
校验逻辑:验证时需同时检查
nbf 和 exp:nbf <= current_time < exp。如果 nbf 未设置,则默认令牌立即生效。
4.5 jti(唯一标识)—— 防重放的最后防线
jti 是一个由签发服务生成的唯一字符串,用于标识单个 JWT。即使两个令牌的载荷完全相同,jti 也必须不同。核心用途:
- 防重放攻击:服务端维护一个已使用
jti的集合(如 Redis Set),令牌被使用后记录jti,后续相同jti的请求直接拒绝。 - 令牌撤销:将
jti加入黑名单,比基于iat的撤销更精确。 - 日志关联:通过
jti串联同一令牌在多个服务间的调用链路。
// 生成 jti(UUID v4)
import "github.com/google/uuid"
claims["jti"] = uuid.New().String()
性能注意:如果每个请求都查询 Redis 检查
jti 是否已使用,会引入额外延迟。建议仅在关键写操作或高安全场景下启用 jti 校验,读操作可跳过。
4.6 生命周期状态机与校验流程
下图展示 JWT 从签发到废弃的完整状态转换,以及各字段在验证阶段的检查点。
4.7 实战建议与常见误区
| 字段 | 推荐做法 | 常见误区 |
|---|---|---|
iat |
始终设置,用于审计和黑名单时间窗口 | 使用客户端时间而非服务端时间 |
exp |
必须设置,有效期不超过业务容忍上限 | 设置为相对时长而非绝对时间戳 |
nbf |
仅在需要延迟生效时设置,默认不设 | 与 iat 混淆,或设置过大的未来时间 |
jti |
高安全场景必设,使用 UUID v4 | 使用固定字符串或可预测序列 |
核心原则:JWT 生命周期管理不是“签发即忘”。
iat 定义起点,nbf 定义可用起点,exp 定义终点,jti 提供精确撤销能力。四者配合使用,才能构建安全的分布式鉴权体系。