exp报过期主因是时间戳单位错误:expiresat必须为秒级int64,误用unixmilli()导致值过大被解析为远期时间;正确写法是jwt.newnumericdate(time.now().add(1*time.hour))。

签发 JWT 时 exp 总报过期?不是 token 真过期了,是时间戳单位错了——jwt.RegisteredClaims.ExpiresAt 要的是秒级 int64,你却传了毫秒值。
ExpiresAt 必须用秒级时间戳,别碰 UnixMilli()
Go 的 jwt/v5 要求 ExpiresAt 是 jwt.NumericDate 类型,底层是秒级 Unix 时间戳。常见错误:
-
time.Now().Add(1 * time.Hour).UnixMilli()❌ 值如1740825123456,验签器当它是“公元 5 万年”,立刻判Token is expired -
"exp": time.Now().Unix()直接塞进map[string]interface{}❌ v5 不做类型推导,字段被静默丢弃或 panic -
ExpiresAt: time.Now().Add(1 * time.Hour).Unix()❌ 类型不匹配,编译可能过,但运行时校验失效
正确写法只有一条路:ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)) ✅
自定义 Claims 必须嵌入 RegisteredClaims
如果你定义了结构体想存 UserID 和标准字段(exp/iss/sub),光写字段没用:
-
type UserClaims { UserID string; ExpiresAt int64 }❌ 不触发任何标准校验,解析后token.Claims是空结构体,字段全零值 -
type UserClaims { UserID string; jwt.RegisteredClaims }✅ 嵌入才能让 v5 自动绑定并校验exp、iat、nbf等 - 调用
jwt.ParseWithClaims时,必须传&UserClaims{}(指针)❌ 传值会导致字段赋值失败
keyFunc 返回密钥必须非 nil,且算法要显式限定
解析 token 后 token.Valid == false,90% 出在密钥或算法上:
-
keyFunc返回nil或空字节切片(比如os.Getenv("JWT_SECRET")为空)→ 验签直接失败 - 没加
jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()})→ 攻击者可把 header 的"alg"改成"none"或"HS384"绕过校验(v5 已移除none,但 alg 不匹配仍会拒收) - 硬编码密钥如
var jwtKey = []byte("my-secret")→ 上线等于裸奔;Docker/K8s 环境优先读文件(如/run/secrets/jwt_key),其次环境变量,且避免出现在命令行参数里
sub 和 iss 不是可选项,漏了中间件就取不到用户 ID
登录接口生成 token 时,exp 和 iat 库会自动填,但这两项必须手动设:
-
sub:必须是稳定不可变的用户标识,比如数据库主键user_id,别用邮箱或昵称(后续改名就断链) -
iss:建议设为服务名(如"auth-api"),多系统共用 JWT 时能区分来源 - 中间件里解析完 token,别直接用
token.Claims.(jwt.MapClaims)["sub"]—— 类型断言失败不报错,结果是nil;稳妥做法是定义结构体 + 指针接收 + 判空
最麻烦的其实是时钟偏移:不同机器时间差几秒,exp/nbf 就可能误判。上线前务必用 chrony 对齐所有服务节点时间,别信本地 time.Now()。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











