package middleware import ( "encoding/json" "net/http" "strings" "time" jwtUtil "sundynix-micro-go/common/utils/jwt" jwtv5 "github.com/golang-jwt/jwt/v5" "github.com/zeromicro/go-zero/core/logx" ) // RefreshTokenHeader 续期后新 Token 放在此响应头里,前端读取后静默替换 const RefreshTokenHeader = "X-Refresh-Token" // AuthMiddleware 网关鉴权 + 自动续期中间件 type AuthMiddleware struct { jwtSecret string whitelist map[string]bool } func NewAuthMiddleware(jwtSecret string, whitelist []string) *AuthMiddleware { wl := make(map[string]bool, len(whitelist)) for _, p := range whitelist { wl[p] = true } return &AuthMiddleware{ jwtSecret: jwtSecret, whitelist: wl, } } // Handle 适配 http.Handler 链(自定义 gateway 使用 http.Handler 链式调用) func (m *AuthMiddleware) Handle(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // OPTIONS 预检直接放行 if r.Method == http.MethodOptions { next.ServeHTTP(w, r) return } // 白名单路径放行(支持精确匹配和 /* 前缀通配) if m.isWhitelisted(r.URL.Path) { next.ServeHTTP(w, r) return } // 解析 Authorization 头 authHeader := r.Header.Get("Authorization") if authHeader == "" { writeUnauthorized(w, "缺少 Authorization 请求头") return } tokenStr := jwtUtil.GetTokenFromHeader(authHeader) if tokenStr == "" { writeUnauthorized(w, "Token 格式错误") return } j := jwtUtil.NewJWT(m.jwtSecret) claims, err := j.ParseToken(tokenStr) if err != nil { logx.Infof("[gateway] JWT 解析失败: %v, path: %s", err, r.URL.Path) writeUnauthorized(w, err.Error()) return } // 将用户信息透传到上游,避免上游重复解析 JWT r.Header.Set("X-User-Id", claims.BaseClaims.ID) r.Header.Set("X-User-Account", claims.BaseClaims.Account) // ---- 滑动窗口续期 ---- if newToken, ok := m.tryRefresh(j, claims); ok { w.Header().Set(RefreshTokenHeader, newToken) logx.Infof("[gateway] Token 已续期, userId: %s", claims.BaseClaims.ID) } next.ServeHTTP(w, r) }) } // tryRefresh 判断是否需要续期,需要则签发新 Token 并返回 // 续期规则:剩余有效时间 < BufferTime → 以原始有效时长(ExpiresAt - NotBefore)重新签发 func (m *AuthMiddleware) tryRefresh(j *jwtUtil.JWT, claims *jwtUtil.CustomClaims) (string, bool) { bufferTime := time.Duration(claims.BufferTime) * time.Second expiresAt := claims.RegisteredClaims.ExpiresAt.Time remaining := time.Until(expiresAt) // 未进入缓冲窗口,无需续期 if remaining >= bufferTime { return "", false } // 计算原始有效时长:ExpiresAt - NotBefore ≈ 当初登录时配置的 activeTimeout notBefore := claims.RegisteredClaims.NotBefore.Time originalDuration := expiresAt.Sub(notBefore) newClaims := jwtUtil.CustomClaims{ BaseClaims: claims.BaseClaims, BufferTime: claims.BufferTime, RegisteredClaims: jwtv5.RegisteredClaims{ Audience: claims.RegisteredClaims.Audience, Issuer: claims.RegisteredClaims.Issuer, NotBefore: jwtv5.NewNumericDate(time.Now()), ExpiresAt: jwtv5.NewNumericDate(time.Now().Add(originalDuration)), }, } newToken, err := j.CreateToken(newClaims) if err != nil { logx.Errorf("[gateway] Token 续期失败: %v", err) return "", false } return newToken, true } // isWhitelisted 支持精确匹配和 /* 前缀通配 func (m *AuthMiddleware) isWhitelisted(path string) bool { if m.whitelist[path] { return true } for p := range m.whitelist { if strings.HasSuffix(p, "/*") && strings.HasPrefix(path, strings.TrimSuffix(p, "*")) { return true } } return false } // writeUnauthorized 返回统一的 401 响应 func writeUnauthorized(w http.ResponseWriter, msg string) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusUnauthorized) _ = json.NewEncoder(w).Encode(map[string]interface{}{ "code": 401, "msg": msg, }) }