JWT本身无状态,但生产环境需补足状态管理:refresh token存Redis、强制校验token.Valid和err、exp校验设leeway、禁用ParseUnverified、Authorization头要规范解析、自定义Claims须实现Valid方法。

直接上结论:JWT 本身是无状态的,但“无状态认证流程”在生产环境里必须补足状态环节——比如 refresh token 存 Redis、access token 过期后拒绝而非静默续期、校验时强制检查 token.Valid 和 err 双条件。纯靠 jwt.ParseWithClaims + HS256 密钥跑通 demo,离可用差至少三步。
jwt.ParseWithClaims 必须配 KeyFunc,不能传裸密钥
很多人写中间件时直接把 jwtKey(比如 []byte("secret"))塞进 ParseWithClaims 第三个参数,这是错的。v5 版本要求必须用 SigningMethod.KeyFunc 返回密钥,否则会静默失败——token.Valid 是 false,但 err 可能为 nil。
- HS256 场景:KeyFunc 必须返回
[]byte,且不能是字符串转义后的结果;从环境变量读取时得写成func(t *jwt.Token) (interface{}, error) { return []byte(os.Getenv("JWT_SECRET")), nil } - RS256 场景:KeyFunc 要返回
*rsa.PublicKey,且签名时私钥必须是配对的*rsa.PrivateKey;类型不匹配不会报错,只会让token.Valid == false - 绝对禁用
jwt.ParseUnverified做权限判断——它不验签名,攻击者可任意改user_id或exp后重发
Authorization Header 解析要防空格和大小写
前端发来的 Authorization: Bearer xxx 实际可能带首尾空格、大小写混用(如 bearer xxx),或中间多空格。Gin 或 net/http 中间件若只做简单字符串切分,很容易漏掉或解析错。
- 先用
strings.TrimSpace清空头尾空白 - 再用
strings.HasPrefix(authHeader, "Bearer ")判断前缀,注意末尾有空格 - 提取 token 时用
strings.TrimPrefix(authHeader, "Bearer "),别手写authHeader[7:]——万一哪天改成BearerToken就崩了 - 如果同时支持 Cookie 模式(如
httpOnly),需明确优先级(建议 Header > Cookie),并单独处理http.Request.Cookie("token")
exp 校验必须设 leeway,不能依赖系统时间同步
客户端和服务端时钟差超过 1 秒,就可能触发 "Token is expired" 错误。别去调服务器时间,也别在代码里手动加减时间戳。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 用
jwt.WithExpirationRequired()+jwt.WithLeeway(30 * time.Second)初始化验证器 -
leeway是容忍窗口,不是延长有效期;它只影响 exp/nbf 校验,不影响实际 token 生效时长 - 自定义
Claims结构体时,务必实现Valid()方法,并在里面显式调用RegisteredClaims.Valid() - 别信“本地开发没问题,上线就报 exp 失败”——大概率是没设 leeway,且测试机和服务时间不同步
refresh token 不该是 JWT,而应走 Redis 一用即废
把 refresh token 也做成 JWT 是典型误区。JWT 无法主动作废,一旦泄露等于长期通行证。真实业务中封号、改密、异地登录踢出,全靠服务端状态控制。
- refresh token 存 Redis,key 为
refresh:<jti_hash></jti_hash>,value 存user_id+ 过期时间(TTL 与 token 一致) - 签发 access token 时生成唯一
jti,取其 SHA256 作为 key 写入 Redis - 每次 refresh 请求,先
GET再DEL,成功才发新 access token;失败则返回 401 - 不要用 JWT 实现 refresh token,除非你明确接受“无法立即踢人”“改密不生效”这四个致命缺陷
最易被忽略的一点:所有中间件里从 context 取用户信息时,必须确认 ctx.Value("user") 不是 nil,且类型断言成功。Gin 的 c.MustGet("user") 会 panic,net/http 的 ctx.Value 返回 interface{} 后不检查类型,都可能导致 runtime panic 或静默降权。

















