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

文章目录

    今天,继续昨天的系列 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 聊聊,或者关注我的个人公众号“大象工具”, 查看更多联系方式