// Filename: internal/middleware/web.go package middleware import ( "crypto/sha256" "encoding/hex" "gemini-balancer/internal/service" "net/http" "os" "strings" "sync" "time" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" ) const ( AdminSessionCookie = "gemini_admin_session" SessionMaxAge = 3600 * 24 * 7 // 7天 CacheTTL = 5 * time.Minute CleanupInterval = 10 * time.Minute // 降低清理频率 SessionRefreshTime = 30 * time.Minute ) // ==================== 缓存层 ==================== type authCacheEntry struct { Token interface{} ExpiresAt time.Time } type authCache struct { mu sync.RWMutex cache map[string]*authCacheEntry ttl time.Duration } var webAuthCache = newAuthCache(CacheTTL) func newAuthCache(ttl time.Duration) *authCache { c := &authCache{ cache: make(map[string]*authCacheEntry), ttl: ttl, } go c.cleanupLoop() return c } func (c *authCache) get(key string) (interface{}, bool) { c.mu.RLock() defer c.mu.RUnlock() entry, exists := c.cache[key] if !exists || time.Now().After(entry.ExpiresAt) { return nil, false } return entry.Token, true } func (c *authCache) set(key string, token interface{}) { c.mu.Lock() defer c.mu.Unlock() c.cache[key] = &authCacheEntry{ Token: token, ExpiresAt: time.Now().Add(c.ttl), } } func (c *authCache) delete(key string) { c.mu.Lock() defer c.mu.Unlock() delete(c.cache, key) } func (c *authCache) cleanupLoop() { ticker := time.NewTicker(CleanupInterval) defer ticker.Stop() for range ticker.C { c.cleanup() } } func (c *authCache) cleanup() { c.mu.Lock() defer c.mu.Unlock() now := time.Now() count := 0 for key, entry := range c.cache { if now.After(entry.ExpiresAt) { delete(c.cache, key) count++ } } if count > 0 { logrus.Debugf("[AuthCache] Cleaned up %d expired entries", count) } } // ==================== 会话刷新缓存 ==================== var sessionRefreshCache = struct { sync.RWMutex timestamps map[string]time.Time }{ timestamps: make(map[string]time.Time), } // 定期清理刷新时间戳 func init() { go func() { ticker := time.NewTicker(1 * time.Hour) defer ticker.Stop() for range ticker.C { sessionRefreshCache.Lock() now := time.Now() for key, ts := range sessionRefreshCache.timestamps { if now.Sub(ts) > 2*time.Hour { delete(sessionRefreshCache.timestamps, key) } } sessionRefreshCache.Unlock() } }() } // ==================== Cookie 操作 ==================== func SetAdminSessionCookie(c *gin.Context, adminToken string) { secure := c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" c.SetSameSite(http.SameSiteStrictMode) c.SetCookie(AdminSessionCookie, adminToken, SessionMaxAge, "/", "", secure, true) } func SetAdminSessionCookieWithAge(c *gin.Context, adminToken string, maxAge int) { secure := c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" c.SetSameSite(http.SameSiteStrictMode) c.SetCookie(AdminSessionCookie, adminToken, maxAge, "/", "", secure, true) } func ClearAdminSessionCookie(c *gin.Context) { c.SetSameSite(http.SameSiteStrictMode) c.SetCookie(AdminSessionCookie, "", -1, "/", "", false, true) } func ExtractTokenFromCookie(c *gin.Context) string { cookie, err := c.Cookie(AdminSessionCookie) if err != nil { return "" } return cookie } // ==================== 认证中间件 ==================== func WebAdminAuthMiddleware(authService *service.SecurityService) gin.HandlerFunc { logger := logrus.New() logger.SetLevel(getLogLevel()) return func(c *gin.Context) { cookie := ExtractTokenFromCookie(c) if cookie == "" { logger.Debug("[WebAuth] No session cookie found") ClearAdminSessionCookie(c) redirectToLogin(c) return } cacheKey := hashToken(cookie) if cachedToken, found := webAuthCache.get(cacheKey); found { logger.Debug("[WebAuth] Using cached token") c.Set("adminUser", cachedToken) refreshSessionIfNeeded(c, cookie) c.Next() return } logger.Debug("[WebAuth] Cache miss, authenticating...") authToken, err := authService.AuthenticateToken(cookie) if err != nil { logger.WithError(err).Warn("[WebAuth] Authentication failed") ClearAdminSessionCookie(c) webAuthCache.delete(cacheKey) redirectToLogin(c) return } if !authToken.IsAdmin { logger.Warn("[WebAuth] User is not admin") ClearAdminSessionCookie(c) webAuthCache.delete(cacheKey) redirectToLogin(c) return } webAuthCache.set(cacheKey, authToken) logger.Debug("[WebAuth] Authentication success, token cached") c.Set("adminUser", authToken) refreshSessionIfNeeded(c, cookie) c.Next() } } func WebAdminAuthMiddlewareWithLogger(authService *service.SecurityService, logger *logrus.Logger) gin.HandlerFunc { return func(c *gin.Context) { cookie := ExtractTokenFromCookie(c) if cookie == "" { logger.Debug("No session cookie found") ClearAdminSessionCookie(c) redirectToLogin(c) return } cacheKey := hashToken(cookie) if cachedToken, found := webAuthCache.get(cacheKey); found { c.Set("adminUser", cachedToken) refreshSessionIfNeeded(c, cookie) c.Next() return } authToken, err := authService.AuthenticateToken(cookie) if err != nil { logger.WithError(err).Warn("Token authentication failed") ClearAdminSessionCookie(c) webAuthCache.delete(cacheKey) redirectToLogin(c) return } if !authToken.IsAdmin { logger.Warn("Token valid but user is not admin") ClearAdminSessionCookie(c) webAuthCache.delete(cacheKey) redirectToLogin(c) return } webAuthCache.set(cacheKey, authToken) c.Set("adminUser", authToken) refreshSessionIfNeeded(c, cookie) c.Next() } } // ==================== 辅助函数 ==================== func hashToken(token string) string { h := sha256.Sum256([]byte(token)) return hex.EncodeToString(h[:]) } func redirectToLogin(c *gin.Context) { if isAjaxRequest(c) { c.JSON(http.StatusUnauthorized, gin.H{ "error": "Session expired", "code": "AUTH_REQUIRED", }) c.Abort() return } originalPath := c.Request.URL.Path if originalPath != "/" && originalPath != "/login" { c.Redirect(http.StatusFound, "/login?redirect="+originalPath) } else { c.Redirect(http.StatusFound, "/login") } c.Abort() } func isAjaxRequest(c *gin.Context) bool { // 检查 Content-Type contentType := c.GetHeader("Content-Type") if strings.Contains(contentType, "application/json") { return true } // 检查 Accept(优先检查 JSON) accept := c.GetHeader("Accept") if strings.Contains(accept, "application/json") && !strings.Contains(accept, "text/html") { return true } // 兼容旧版 XMLHttpRequest return c.GetHeader("X-Requested-With") == "XMLHttpRequest" } func refreshSessionIfNeeded(c *gin.Context, token string) { tokenHash := hashToken(token) sessionRefreshCache.RLock() lastRefresh, exists := sessionRefreshCache.timestamps[tokenHash] sessionRefreshCache.RUnlock() if !exists || time.Since(lastRefresh) > SessionRefreshTime { SetAdminSessionCookie(c, token) sessionRefreshCache.Lock() sessionRefreshCache.timestamps[tokenHash] = time.Now() sessionRefreshCache.Unlock() } } func getLogLevel() logrus.Level { level := os.Getenv("LOG_LEVEL") switch strings.ToLower(level) { case "debug": return logrus.DebugLevel case "warn": return logrus.WarnLevel case "error": return logrus.ErrorLevel default: return logrus.InfoLevel } } // ==================== 工具函数 ==================== func GetAdminUserFromContext(c *gin.Context) (interface{}, bool) { return c.Get("adminUser") } func InvalidateTokenCache(token string) { tokenHash := hashToken(token) webAuthCache.delete(tokenHash) // 同时清理刷新时间戳 sessionRefreshCache.Lock() delete(sessionRefreshCache.timestamps, tokenHash) sessionRefreshCache.Unlock() } func ClearAllAuthCache() { webAuthCache.mu.Lock() webAuthCache.cache = make(map[string]*authCacheEntry) webAuthCache.mu.Unlock() sessionRefreshCache.Lock() sessionRefreshCache.timestamps = make(map[string]time.Time) sessionRefreshCache.Unlock() } // ==================== 调试工具 ==================== type SessionInfo struct { HasCookie bool `json:"has_cookie"` IsValid bool `json:"is_valid"` IsAdmin bool `json:"is_admin"` IsCached bool `json:"is_cached"` LastActivity string `json:"last_activity"` } func GetSessionInfo(c *gin.Context, authService *service.SecurityService) SessionInfo { info := SessionInfo{ HasCookie: false, IsValid: false, IsAdmin: false, IsCached: false, LastActivity: time.Now().Format(time.RFC3339), } cookie := ExtractTokenFromCookie(c) if cookie == "" { return info } info.HasCookie = true cacheKey := hashToken(cookie) if _, found := webAuthCache.get(cacheKey); found { info.IsCached = true } authToken, err := authService.AuthenticateToken(cookie) if err != nil { return info } info.IsValid = true info.IsAdmin = authToken.IsAdmin return info } func GetCacheStats() map[string]interface{} { webAuthCache.mu.RLock() cacheSize := len(webAuthCache.cache) webAuthCache.mu.RUnlock() sessionRefreshCache.RLock() refreshSize := len(sessionRefreshCache.timestamps) sessionRefreshCache.RUnlock() return map[string]interface{}{ "auth_cache_entries": cacheSize, "refresh_cache_entries": refreshSize, "ttl_seconds": int(webAuthCache.ttl.Seconds()), "cleanup_interval": int(CleanupInterval.Seconds()), "session_refresh_time": int(SessionRefreshTime.Seconds()), } }