不同语言生成 JWT 时间戳单位不一致怎么导致验证失败?

文章导读
Go 语言 jwt.RegisteredClaims.ExpiresAt 误用 UnixMilli() 产生毫秒级时间戳(如 1740825123456),验证器按秒级解析会将其判定为公元 5 万多年后的时间,导致 token 立即过期失效。
📋 目录
  1. 原因分析
  2. 解决方案
  3. 注意事项
  4. 参考来源
A A

不同语言生成 JWT 时间戳单位不一致怎么导致验证失败?

核心结论:Go 语言 jwt.RegisteredClaims.ExpiresAt 误用 UnixMilli() 产生毫秒级时间戳(如 1740825123456),验证器按秒级解析会将其判定为公元 5 万多年后的时间,导致 token 立即过期失效。

原因分析

JWT 标准规范中 exp、iat、nbf 等时间字段应采用秒级 Unix 时间戳,但不同语言/库的实现存在精度差异。根据 2026 年 2 月 27 日的技术资料,JWT-go 默认使用秒级精度,但提供了调整时间精度的选项。Go 的 golang-jwt/jwt/v5 库要求 ExpiresAt 必须是 jwt.NumericDate 类型,底层是 int64 秒级 Unix 时间戳。若开发者误用 time.Now().Add(1 * time.Hour).UnixMilli(),产生的毫秒值会被验证器按秒解析,造成时间偏移 1000 倍。

跨语言场景中问题更隐蔽:Java 端使用 java-jwt 3.x 版本生成秒级时间戳,而 Node.js 的 jsonwebtoken 9.0.0 默认也使用秒级,但部分 PHP 库(如 firebase/php-jwt 6.x)在特定配置下可能处理毫秒值,导致签名验证时时间校验失败。

解决方案

Go 语言正确实现

使用 golang-jwt/jwt/v5 包,时间字段必须用 jwt.NewNumericDate() 包装:

不同语言生成 JWT 时间戳单位不一致怎么导致验证失败?
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)) ✅ 正确
ExpiresAt: time.Now().Add(1 * time.Hour).Unix() ❌ v5 会静默忽略该字段
ExpiresAt: time.Now().Add(1 * time.Hour).UnixMilli() ❌ 值过大导致立即过期

解析时必须传入指针类型 claims 结构体,且需嵌入 jwt.RegisteredClaims:

type UserClaims struct {
    UserID string
    jwt.RegisteredClaims  ✅ 必须嵌入才能触发标准校验
}

Node.js 正确实现

根据 2026 年 3 月 24 日的验证代码,jsonwebtoken 库需确保签发和验证使用相同密钥:

const token = jwt.sign({ userId: '123' }, secret, { expiresIn: '1h' });
const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });

必须显式指定 algorithms 数组,否则攻击者可篡改 header 中的 alg 字段绕过校验。

不同语言生成 JWT 时间戳单位不一致怎么导致验证失败?

Java/PHP 跨语言兼容

根据 2025 年 7 月 8 日的跨语言实战资料,Java 生成的 JWT 在 PHP 解析时签名验证失败,原因是不同版本对密钥处理差异。解决方案:

  • Java 端使用 org.bitbucket.b_c:jose4j 0.9.6+ 确保密钥字节编码一致
  • PHP 端使用 firebase/php-jwt 6.5.0+,密钥需用 Encoding.UTF8.GetBytes() 统一编码
  • 双方均强制使用 HS256 算法,禁用 none 算法

注意事项

根据多个技术论坛和 GitHub Issue 的真实反馈,以下坑点需特别注意:

  • token.Valid == false 但无明确异常:调用 jwt.ParseWithClaims 后必须显式校验 token.Valid,解析成功不等于签名有效。常见原因是 keyFunc 返回了错误密钥或未限制合法算法。
  • 401 Unauthorized 但日志无堆栈:C# 中使用 microsoft.identitymodel.tokens 6.0+ 时,ValidateLifetime = true 且服务器时间偏差 >300 秒会导致 nbf/exp 校验提前拒绝。
  • Claims 结构体字段全零值:自定义 claims 若不嵌入 jwt.RegisteredClaims,v5 不会自动触发 exp/iss/aud 等标准校验,解析后 token.Claims 实际是空结构体。
  • 密钥硬编码漏洞:把 var jwtKey = "my-secret" 写进源码等于把房门钥匙刻在门框上,上线会被安全扫描工具直接检出。
  • 旧版 dgrijalva/jwt-go 存在 alg=none 漏洞:必须使用带/v5 后缀的 golang-jwt/jwt/v5 包,该版本已移除 none 算法支持。

参考来源

来源:CSDN 博客 - Go 语言 JWT Token 生成验证教程【推荐】(2026 年 4 月 12 日)

不同语言生成 JWT 时间戳单位不一致怎么导致验证失败?

来源:知乎技术专栏 - Golang 如何生成和验证 JWT(2026 年 4 月 14 日)

来源:掘金社区 - JWT-go 跨语言兼容终极指南(2026 年 2 月 27 日)

来源:博客园 - Java JWT 签名不一致问题:版本坑点与 PHP 跨语言兼容实战(2025 年 7 月 8 日)