// Filename: internal/webhandlers/auth_handler.go package webhandlers import ( "gemini-balancer/internal/middleware" "gemini-balancer/internal/service" "net/http" "strings" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" ) // WebAuthHandler Web 认证处理器 type WebAuthHandler struct { securityService *service.SecurityService logger *logrus.Logger } // NewWebAuthHandler 创建 WebAuthHandler func NewWebAuthHandler(securityService *service.SecurityService) *WebAuthHandler { logger := logrus.New() logger.SetLevel(logrus.InfoLevel) return &WebAuthHandler{ securityService: securityService, logger: logger, } } // ShowLoginPage 显示登录页面 func (h *WebAuthHandler) ShowLoginPage(c *gin.Context) { errMsg := c.Query("error") // 验证重定向路径(防止开放重定向攻击) redirectPath := h.validateRedirectPath(c.Query("redirect")) // 如果已登录,直接重定向 if cookie := middleware.ExtractTokenFromCookie(c); cookie != "" { if _, err := h.securityService.AuthenticateToken(cookie); err == nil { c.Redirect(http.StatusFound, redirectPath) return } } c.HTML(http.StatusOK, "auth.html", gin.H{ "error": errMsg, "redirect": redirectPath, }) } // HandleLogin 已废弃(项目无用户名系统) func (h *WebAuthHandler) HandleLogin(c *gin.Context) { c.Redirect(http.StatusFound, "/login?error=DEPRECATED_LOGIN_METHOD") } // HandleLogout 处理登出请求 func (h *WebAuthHandler) HandleLogout(c *gin.Context) { cookie := middleware.ExtractTokenFromCookie(c) if cookie != "" { // 尝试获取 Token 信息用于日志 authToken, err := h.securityService.AuthenticateToken(cookie) if err == nil { h.logger.WithFields(logrus.Fields{ "token_id": authToken.ID, "client_ip": c.ClientIP(), }).Info("User logged out") } else { h.logger.WithField("client_ip", c.ClientIP()).Warn("Logout with invalid token") } // 使缓存失效 middleware.InvalidateTokenCache(cookie) } else { h.logger.WithField("client_ip", c.ClientIP()).Debug("Logout without session cookie") } // 清除 Cookie middleware.ClearAdminSessionCookie(c) // 重定向到登录页 c.Redirect(http.StatusFound, "/login") } // validateRedirectPath 验证重定向路径(防止开放重定向攻击) func (h *WebAuthHandler) validateRedirectPath(path string) string { defaultPath := "/dashboard" if path == "" { return defaultPath } // 只允许内部路径 if !strings.HasPrefix(path, "/") || strings.HasPrefix(path, "//") { h.logger.WithField("path", path).Warn("Invalid redirect path blocked") return defaultPath } // 白名单验证 allowedPaths := []string{ "/dashboard", "/keys", "/settings", "/logs", "/tasks", "/chat", } for _, allowed := range allowedPaths { if strings.HasPrefix(path, allowed) { return path } } return defaultPath }