Golang JWT Token 升级之二,RegisteredClaims 的使用细节

更新日期: 2025-08-26 阅读次数: 52 字数: 1132 分类: golang

今天,继续昨天的系列 Golang JWT 库升级,RegisteredClaims 取代 StandardClaims。 不得不说,golang-jwt/jwt 官方文档太晦涩了,很多细节都需要自己去探索。

RegisteredClaims 过期时间的设置

之前的 StandardClaims 的 exp 字段是 int64 类型,表示 Unix 时间戳。例如:

claims["exp"] = time.Now().Add(time.Second * sec).Unix()   // 废弃 ⚠️

但是,最新的 RegisteredClaims 的 exp 字段(ExpiresAt)是 *jwt.NumericDate 类型:

type NumericDate struct {
    time.Time
}

type RegisteredClaims struct {
    Issuer string `json:"iss,omitempty"`
    Subject string `json:"sub,omitempty"`
    Audience ClaimStrings `json:"aud,omitempty"`
    ExpiresAt *NumericDate `json:"exp,omitempty"`
    NotBefore *NumericDate `json:"nbf,omitempty"`
    IssuedAt *NumericDate `json:"iat,omitempty"`
    ID string `json:"jti,omitempty"`
}

设置 ExpiresAt 字段示例:

now := time.Now()
claims := &jwt.RegisteredClaims{
    Issuer: "Test",
    ExpiresAt: &jwt.NumericDate{Time: now.Add(time.Hour * 24)},
}

为何这里使用 *NumericDate

这是一个非常有趣的问题,为何要使用 *NumericDate 代表时间,而不是直接使用 *time.Time 或者 time.Time,或者是 int64 呢?

如果直接使用 time.Time,会导致序列化之后,变成了字符串形式,形如:"2024-12-31T00:00:00Z"。 但是 JWT Token 的 RFC 7519 规范中,要求 exp 字段必须是一个数字,表示 Unix 时间戳。单位为秒。 封装一层,可以方便的自定义序列化和反序列化逻辑。

使用指针 (*NumericDate) 的原因:区分“零值”和“未设置”。 这是一个非常经典的Go语言设计模式,用于处理可选字段。 如果一个 time.Time 字段是零值,我们无法判断是用户故意设置了一个远古的日期,还是这个字段根本就没设置。 对于 exp 来说,0 是一个有意义的值(1970年1月1日),但它也经常是未初始化变量的值。如果将其编码到JWT中,Token会在1970年就“过期”,这显然是错误的。

使用指针 (*NumericDate) 可以完美解决这个问题:

  • 如果指针为 nil: 表示这个声明未被设置。在序列化(编码)成JWT时,这个字段会被完全忽略(因为标签有 omitempty)。
  • 如果指针指向一个有效的 NumericDate: 表示这个声明已被显式设置。在序列化时,会将其编码为对应的数字时间戳。

至于为何不使用 int64,估计是为了减少类型转换的麻烦。

不过,目前的实现方案确实有点绕,费脑子。

MapClaims: 完全自定义的 Claims

如果不想使用 RegisteredClaims 的内置字段,可以使用 MapClaims 来完全自定义 Claims 字段。

var (
  key *ecdsa.PrivateKey
  t   *jwt.Token
  s   string
)

key = /* Load key from somewhere, for example a file */
t = jwt.NewWithClaims(jwt.SigningMethodES256,
  jwt.MapClaims{
    "iss": "my-auth-server",
    "sub": "john",
    "foo": 2,
  })
s = t.SignedString(key)

但是,我的使用场景来看,RegisteredClaims 的内置字段已经足够满足需求了。

扩展字段

或者想基于 RegisteredClaims 添加自定义字段,可以这样做:

type MyCustomClaims struct {
	UID  int    `json:"uid"`
	Role string `json:"role"`
	jwt.RegisteredClaims
}

参考资料

  • Github 仓库:https://github.com/golang-jwt/jwt
  • 如何创建 JWT Token: https://golang-jwt.github.io/jwt/usage/create/
  • 解析及验证:https://pkg.go.dev/github.com/golang-jwt/jwt/v5#example-Parse-Hmac

关于作者 🌱

我是来自山东烟台的一名开发者,有感兴趣的话题,或者软件开发需求,欢迎加微信 zhongwei 聊聊, 查看更多联系方式