Golang实现JWT双Token自动刷新机制

📅 2026/7/19 10:19:39
Golang实现JWT双Token自动刷新机制
1. 项目背景与核心需求在基于Golang的Web应用中JWT(JSON Web Token)是最常用的身份验证机制之一。传统JWT实现存在一个明显的体验缺陷当用户持续活跃操作时Token仍然会在预设的固定时间后过期导致操作中断。这就像在电影院看电影时每30分钟就要重新检票一次——即使你一直坐在座位上专注观影。我们需要的是一种智能的Token有效期管理机制当用户持续活跃时Token有效期应自动延长仅在用户长时间无操作时才真正过期。这种机制能显著提升用户体验同时保持足够的安全性。2. 技术方案设计2.1 基础架构设计实现方案采用双Token机制Access Token短期有效(如30分钟)用于API鉴权Refresh Token长期有效(如7天)存储在HttpOnly的Cookie中关键流程用户登录后同时获取两个Token每次请求携带Access Token当Access Token即将过期时(通过中间件检测)使用Refresh Token获取新Token新Token通过响应头返回前端自动更新2.2 核心代码实现// TokenManager 管理JWT的生成与刷新 type TokenManager struct { secretKey string accessExpire int64 refreshExpire int64 } // GenerateTokens 生成双Token func (m *TokenManager) GenerateTokens(userID int64) (string, string, error) { now : time.Now().Unix() // 生成Access Token accessClaims : jwt.MapClaims{ user_id: userID, exp: now m.accessExpire, } accessToken : jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) // 生成Refresh Token refreshClaims : jwt.MapClaims{ user_id: userID, exp: now m.refreshExpire, } refreshToken : jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims) return accessToken.SignedString([]byte(m.secretKey)), refreshToken.SignedString([]byte(m.secretKey)) } // RefreshAccessToken 刷新Access Token func (m *TokenManager) RefreshAccessToken(refreshToken string) (string, error) { token, err : jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) { return []byte(m.secretKey), nil }) if err ! nil || !token.Valid { return , errors.New(invalid refresh token) } claims, ok : token.Claims.(jwt.MapClaims) if !ok { return , errors.New(invalid token claims) } userID, ok : claims[user_id].(float64) if !ok { return , errors.New(invalid user_id in token) } return m.GenerateTokens(int64(userID)) }3. 关键实现细节3.1 Token刷新中间件func TokenRefreshMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 从Header获取Access Token authHeader : r.Header.Get(Authorization) if authHeader { next.ServeHTTP(w, r) return } // 解析Token但不立即验证过期(为了获取claims) token, _ : jwt.Parse(authHeader, func(token *jwt.Token) (interface{}, error) { return []byte(secretKey), nil }) if claims, ok : token.Claims.(jwt.MapClaims); ok { exp : int64(claims[exp].(float64)) now : time.Now().Unix() // 如果Token将在5分钟内过期则刷新 if exp-now 300 { refreshToken, err : r.Cookie(refresh_token) if err nil { newAccessToken, err : tokenManager.RefreshAccessToken(refreshToken.Value) if err nil { w.Header().Set(New-Access-Token, newAccessToken) } } } } next.ServeHTTP(w, r) }) }3.2 前端Token自动更新// 封装fetch请求 async function authFetch(url, options {}) { const response await fetch(url, options); // 检查是否有新Token const newToken response.headers.get(New-Access-Token); if (newToken) { localStorage.setItem(access_token, newToken); } return response; }4. 安全增强措施4.1 Refresh Token保护必须设置为HttpOnly和Secure的Cookie设置SameSiteStrict防止CSRF攻击服务端维护已撤销Token的黑名单// 示例安全的Cookie设置 http.SetCookie(w, http.Cookie{ Name: refresh_token, Value: refreshToken, HttpOnly: true, Secure: true, SameSite: http.SameSiteStrictMode, MaxAge: int(refreshExpire), Path: /, })4.2 并发请求处理当多个并发请求同时触发Token刷新时需要防止重复刷新var refreshMutex sync.Mutex{} func (m *TokenManager) SafeRefresh(refreshToken string) (string, error) { refreshMutex.Lock() defer refreshMutex.Unlock() // 检查缓存中是否已有新Token if newToken, ok : tokenCache.Get(refreshToken); ok { return newToken.(string), nil } // 执行实际刷新 newToken, err : m.RefreshAccessToken(refreshToken) if err ! nil { return , err } // 缓存新Token tokenCache.Set(refreshToken, newToken, 5*time.Minute) return newToken, nil }5. 性能优化与注意事项5.1 Token验证性能JWT验证是CPU密集型操作在高并发场景下需要注意使用高效的签名算法(HS256/RS256)考虑使用本地缓存验证结果避免在中间件中进行完整的Token解析// 快速验证Token是否过期(不验证签名) func IsTokenExpired(tokenString string) bool { parts : strings.Split(tokenString, .) if len(parts) ! 3 { return true } claimsBytes, _ : base64.RawURLEncoding.DecodeString(parts[1]) var claims struct { Exp int64 json:exp } json.Unmarshal(claimsBytes, claims) return time.Now().Unix() claims.Exp }5.2 实际部署经验时钟偏移问题确保所有服务器时间同步建议配置NTP服务Token大小控制避免在Token中存储过多数据通常只放用户ID和必要角色密钥轮换策略定期更换签名密钥旧密钥应保留一段时间用于平滑过渡监控与日志记录Token刷新事件便于排查问题和分析使用模式重要提示Refresh Token比Access Token更敏感必须严格保护。建议实现以下安全措施绑定Refresh Token到特定设备/IP设置使用次数限制提供用户可见的活跃会话管理6. 测试策略6.1 单元测试要点func TestTokenRefresh(t *testing.T) { manager : NewTokenManager(secret, 300, 3600*24) // 生成测试Token access, refresh, err : manager.GenerateTokens(123) if err ! nil { t.Fatalf(生成Token失败: %v, err) } // 测试正常刷新 newAccess, err : manager.RefreshAccessToken(refresh) if err ! nil { t.Errorf(刷新Token失败: %v, err) } // 测试过期Refresh Token time.Sleep(2 * time.Second) // 确保Token过期 _, err manager.RefreshAccessToken(refresh) if err nil { t.Error(过期Refresh Token不应刷新成功) } }6.2 集成测试场景连续API调用期间自动刷新Token并发请求时的Token刷新处理不同设备间的Token互斥网络延迟情况下的Token交接服务重启后的Token持久性7. 替代方案比较7.1 会话Cookie方案优点无需前端特殊处理更简单的实现缺点不利于前后端分离架构跨域限制更多更难实现细粒度的控制7.2 无状态Token方案优点完全无状态扩展性好灵活的客户端存储方式缺点更难实现即时撤销Token大小可能成为问题需要更复杂的安全措施在实际项目中我们选择混合方案短期Access Token 受保护的Refresh Token在用户体验和安全性之间取得平衡。