415 lines
9.4 KiB
Go
415 lines
9.4 KiB
Go
// 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()),
|
||
}
|
||
}
|