双token机制是保障jwt安全续期的必要方案,需结合redis状态管理、动态密钥校验、前端promise共享及严格刷新流程,缺一不可。

Go 里 JWT 自动续期不是靠前端计时器或后端无状态校验就能稳住的,必须用双 Token(accessToken + refreshToken)+ Redis 状态管理 + 前端 Promise 共享,缺一环就可能在高并发下出现“多个 401 后突然 403”或用户被踢两次。
jwt.Parse 默认不校验 exp,必须显式加 jwt.WithValidTime()
很多人发现 token.Valid 总是 true,哪怕 payload 里写了 "exp" —— 因为 jwt.Parse 默认跳过时间验证。不加选项,token.Claims 里的 ExpiresAt 字段也不会被自动解析或校验。
- 正确写法:
token, err := jwt.Parse(tokenString, keyFunc, jwt.WithValidTime()) - 之后必须手动判断:
if err != nil || !token.Valid,不能只看err -
keyFunc应根据token.Header["kid"]动态选密钥,避免密钥轮换后旧 Token 还能通过校验 - 别直接断言
token.Claims是jwt.MapClaims,先类型断言再判空,否则 panic
refreshToken 必须存 Redis、单次有效、绑定 fingerprint
JWT 本身无吊销能力。refreshToken 若纯无状态(比如只签个 long-exp 的字符串),泄露即等于永久通行证。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- Redis Key 格式建议:
refresh:{jti},值为{user_id}:{fingerprint_hash}(fingerprint_hash推荐用sha256(fmt.Sprintf("%s:%s", r.UserAgent(), realIP))生成) - 验证时:先
GET,命中则立刻DEL(强制单次使用),再生成新对并写入新jti - 新
refreshToken的exp设为 7 天,但 Redis TTL 必须严格同步该时间;漏掉DEL或复用旧jti,等于开无限续期后门 - 别把
refreshToken塞进 Cookie 自动发——它应走独立 Header,如X-Refresh-Token
前端拦截器必须共享 Promise,不能各自重试
点一次按钮触发多个并发请求,全卡在 401 边界,如果每个都调 /auth/refresh,服务端会生成多对新 Token,前端无法确定哪个有效,Authorization header 覆盖混乱,后续请求大概率 403。
- 维护一个全局变量
refreshPromise,首次401时触发刷新并缓存 Promise - 后续同批
401全部await refreshPromise,而非发新请求 - 刷新成功后,用新
accessToken重放原始请求;失败则清空登录态(Cookie 或内存 token) - 长连接(WebSocket/gRPC)场景下,服务端需主动预推送新 Token(如
exp前 5 分钟),不能等过期才响应
刷新接口返回的新 refreshToken 必须立即生效,旧值在 Redis 中已不可查
最易被忽略的一点:很多实现只写入新 refreshToken,却忘了删旧 key。结果旧值还在 Redis 里,攻击者截获一次就能无限续期。
Go 刷新接口里,关键动作顺序不能错:解析 → 校验签名和 exp → 提取 jti 和 fingerprint → GET Redis → 不匹配或不存在直接拒绝 → 命中则 DEL → 生成新 accessToken 和新 refreshToken(新 jti 全局唯一)→ 写入新 key。任何一步跳过或颠倒,安全模型就塌了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










