核心是:结构体须嵌入jwt.RegisteredClaims、时间字段须用jwt.NewNumericDate()包装、验签后必须检查token.Valid;否则易因类型错误导致expired或Valid==false。

用 github.com/golang-jwt/jwt/v5 生成和验证 JWT,核心就三件事:结构体必须嵌入 jwt.RegisteredClaims、时间字段必须用 jwt.NewNumericDate() 包装、验签后必须检查 token.Valid —— 漏掉任意一条,token 就可能“看起来成功”但实际被绕过或立刻失效。
为什么 token 总是报 expired 或 Valid == false?
最常见原因是时间戳单位或类型不对。Go 的 jwt.RegisteredClaims.ExpiresAt 必须是 jwt.NumericDate 类型(底层是 int64 秒级 Unix 时间戳),不是 time.Time,也不是毫秒值。
-
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()❌ 值是毫秒(如 1740825123456),验证器按秒解析,当成公元 5 万年后,立刻判定过期 - 没设
IssuedAt或NotBefore,而验证端启用了WithValidate(true),会导致 iat/nbf 校验失败
如何定义 Claims 结构体才不会丢 exp/iat/aud 校验?
不能只写自定义字段,也不能用裸 map[string]interface{}。v5 要求标准字段必须通过嵌入 jwt.RegisteredClaims 显式声明,否则 ParseWithClaims 不会触发任何时间或范围校验。
- 正确写法:
type UserClaims struct { UserID string `json:"user_id"` Role string `json:"role"` jwt.RegisteredClaims } - 错误写法:
type UserClaims struct { UserID string; ExpiresAt int64 }❌ 字段名不匹配、无类型包装、不触发校验 - 错误写法:
jwt.MapClaims{"user_id": "123", "exp": time.Now().Unix()}❌ 手动塞exp是int64也不行,v5 不识别非结构体中的标准字段 - 解析时必须传指针:
&UserClaims{},传值会导致字段赋值失败
keyFunc 返回密钥时有哪些硬性限制?
keyFunc 不是随便返回字符串或字面量的地方。它必须返回 []byte,且长度 ≥ 32 字节;还要校验算法是否合法,否则可能被 alg=none 攻击绕过。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 不能写:
func(token *jwt.Token) (interface{}, error) { return "my-secret", nil }❌ 类型错误:cannot use string as jwt.Keyfunc - 不能写:
return []byte("short")❌ 长度仅 5 字节,crypto/hmac会 panic:invalid key size - 必须写:
func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } secret := os.Getenv("JWT_SECRET") if len(secret) - 调用
ParseWithClaims时务必加jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}),否则攻击者可篡改 header 中的alg字段绕过校验
为什么 ParseWithClaims 不报错,但业务逻辑还是放行了过期 token?
因为 jwt.ParseWithClaims 成功只代表 token 格式合法、签名可解密;是否过期、是否被篡改、是否在有效时间窗口内,全靠 token.Valid 字段触发最终校验。这个字段必须手动判断,不能省略。
- 错误写法:
if err == nil { /* 直接处理 claims */ }❌ 即使err == nil,token.Valid仍可能是false - 正确写法:
if err != nil || !token.Valid { return nil, ErrTokenInvalid }✅ 双重保险 - 解析后取字段前,必须做类型断言:
claims, ok := token.Claims.(*UserClaims),ok == false说明结构体不匹配(比如生成用MapClaims,解析用*UserClaims) - 断言成功后,仍要检查关键字段是否为空:
if claims.UserID == "",防止伪造空用户
真正容易被忽略的点是:密钥加载路径和时钟源。生产环境别从 os.Getenv 直接读密钥——进程启动时命令行参数可能泄露;优先挂载文件(如 /run/secrets/jwt_key)。验证时若服务器时间不同步,exp 校验会误判,建议用 jwt.WithTimeFunc(time.Now().UTC) 统一时区,并在中间件里容忍几秒偏差。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










