双 token 机制是 jwt 在 golang 生产环境的底线配置,必须严格分离 access/refresh token 的解析、签名、存储与刷新逻辑,否则将引发并发刷新、权限越界、防重放失效等真实故障。

双 Token(Access + Refresh)不是可选项,而是 JWT 在 Golang 生产环境的底线配置。单靠 time.Now().After(claims.ExpiresAt) 做前端刷新判断,必然触发并发重复刷新、旧 Refresh Token 被复用、用户表单提交中断——这不是理论风险,是每次高并发登录或批量请求时真实发生的故障。
ParseAccessToken 和 ParseRefreshToken 必须完全隔离
共用一个解析函数但不区分密钥或 Claims 结构,会直接导致权限越界或静默放行非法凭证。
-
ParseAccessToken()只校验exp、iss,用accessSecret签名,返回user_id和基础权限字段,不关心jti或设备指纹 -
ParseRefreshToken()必须显式使用refreshSecret,且只接受jwt.StandardClaims或自定义结构体(含jti、fingerprint),调用claims.VerifyExpiresAt(now, true)严格比对过期时间 - 绝对不能用
jwt.MapClaims解析 Refresh Token:它绕过字段类型校验,jti缺失或被传为 int 会导致防重放逻辑失效 - HTTP Header 必须分离:
Authorization: Bearer <access_token></access_token>与X-Refresh-Token: <refresh_token></refresh_token>,避免中间件误判
Refresh Token 必须绑定设备指纹并原子消费
JWT 本身无吊销能力,不落库 = 允许无限续期。Redis 不是可选组件,而是 Refresh Token 的状态中枢。
- Redis Key 格式固定为
refresh:{user_id}:{fingerprint_hash},其中fingerprint_hash是sha256(fmt.Sprintf("%s:%s", r.UserAgent(), realIP)),不能省略 IP - 签发时用
SETEX写入,TTL 设为硬性值(如 7 天),禁用EXPIRE滑动更新——否则泄露 Token 后攻击者可不断延长有效期 - 刷新接口第一行必须执行
DEL refresh:{user_id}:{fingerprint_hash};返回 0 表示该设备已被登出或换绑,直接拒掉,不再解析 JWT - 成功生成新 Token 对后,才写入新 Key;整个流程不查用户表、不读 request.Body 里的 user_id——唯一可信来源是解析出的
claims["user_id"]和claims["jti"]
前端拦截器必须共享 Promise,不能各自重试
多个并发请求同时收到 401,若每个都独立调 /auth/refresh,服务端 Redis 的 DEL 原子性会让仅一个成功,其余全失败,前端则陷入“刷新失败 → 清态 → 表单丢失”死循环。
- 全局声明
let refreshPromise: Promise<string> | null = null</string> - 首次 401 触发
refreshPromise = fetch("/auth/refresh", {...}).then(res => res.json()).then(data => data.access_token) - 后续所有 401 都执行
await refreshPromise,而非再发请求 - 刷新成功后,用新
access_token重放原始请求;失败则置refreshPromise = null并清登录态 - 切忌在 Vue/React 的 axios interceptor 里写
if (error.response.status === 401) { return refreshToken() }—— 这等于没排队
长连接场景下不能复用 HTTP 刷新逻辑
WebSocket 或 gRPC streaming 握手完成后,无法再通过响应头(如 Set-Cookie)下发新 Token。等过期再处理就晚了,必须预刷新。
- 服务端在每个连接协程中维护
refreshTimer *time.Timer,触发点设为exp - 5 分钟(留网络余量) - 触发时生成新 Access Token,通过业务消息帧(如
{"type":"token_refresh","token":"..."})主动推送 - 客户端收到后,先校验新 Token 的
exp和签名,再原子替换内存中的旧值——不能直接覆盖,否则解析失败会导致后续全 401 - 连接断开时务必调
refreshTimer.Stop(),否则 goroutine 泄漏+panic 写已关闭连接 - 别试图在长连接里调
/auth/refreshHTTP 接口——这破坏连接语义,且无法保证消息顺序
最易被忽略的点是:Refresh Token 的 jti 必须由服务端生成 UUID,且每次刷新都换新;前端传来的任何 jti 都不可信。哪怕只漏掉一次 DEL 或复用同一密钥,整个双 Token 机制就退化成单 Token 的脆弱模型。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











