From edb63eb7a3d6827dff94b849a98a9053d0f3440c Mon Sep 17 00:00:00 2001 From: nanako <469449812@qq.com> Date: Mon, 17 Nov 2025 11:42:17 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=89=8D=E7=AB=AF=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=AF=86=E7=A0=81=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/handlers.go | 29 ++++++++- backend/go.mod | 1 + backend/go.sum | 2 + backend/internal/config/config.go | 3 + backend/internal/middleware/auth.go | 92 ++++++++++++--------------- backend/main.go | 12 ++-- frontend/src/App.jsx | 21 +++++- frontend/src/components/LoginPage.jsx | 48 ++++++++++++++ frontend/src/lib/api.js | 9 +-- 9 files changed, 154 insertions(+), 63 deletions(-) create mode 100644 frontend/src/components/LoginPage.jsx diff --git a/backend/api/handlers.go b/backend/api/handlers.go index 4b4a519..35494d2 100644 --- a/backend/api/handlers.go +++ b/backend/api/handlers.go @@ -2,6 +2,7 @@ package api import ( "ai-gateway/internal/billing" + "ai-gateway/internal/middleware" "ai-gateway/internal/models" "ai-gateway/internal/scheduler" "bytes" @@ -16,8 +17,9 @@ import ( // APIHandler 持有数据库连接并处理API请求 type APIHandler struct { - DB *gorm.DB + DB *gorm.DB LogCleaner *scheduler.LogCleaner + WebUIPassword string } // RequestLogListItem 精简的请求日志列表项(不包含RequestBody和ResponseBody) @@ -670,3 +672,28 @@ func (h *APIHandler) DeleteAPIKeyHandler(c *gin.Context) { "id": apiKey.ID, }) } + +type LoginRequest struct { + Password string `json:"password"` +} + +func (h *APIHandler) LoginHandler(c *gin.Context) { + var req LoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) + return + } + + if req.Password != h.WebUIPassword { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid password"}) + return + } + + token, err := middleware.GenerateJWT() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) + return + } + + c.JSON(http.StatusOK, gin.H{"token": token}) +} diff --git a/backend/go.mod b/backend/go.mod index 87a62ad..9d98895 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -22,6 +22,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/uuid v1.3.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect diff --git a/backend/go.sum b/backend/go.sum index 2e42637..46120a7 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -42,6 +42,8 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 45e251f..8a8f93c 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -42,6 +42,7 @@ type AppConfig struct { Environment string `json:"environment"` LogLevel string `json:"log_level"` LogInDB bool `json:"log_in_db"` + WebUIPassword string `json:"web_ui_password"` } // DefaultConfig 默认配置 @@ -62,6 +63,7 @@ func DefaultConfig() *Config { Version: "1.0.0", Environment: "production", LogLevel: "info", + WebUIPassword: "admin", }, } } @@ -166,4 +168,5 @@ func (c *Config) Print() { log.Printf(" 📱 应用名称: %s v%s", c.App.Name, c.App.Version) log.Printf(" 🌍 运行环境: %s", c.App.Environment) log.Printf(" 📝 日志级别: %s", c.App.LogLevel) + log.Printf(" 🔑 Web UI 密码: %s", c.App.WebUIPassword) } diff --git a/backend/internal/middleware/auth.go b/backend/internal/middleware/auth.go index b80fdf2..14ed8ac 100644 --- a/backend/internal/middleware/auth.go +++ b/backend/internal/middleware/auth.go @@ -3,75 +3,67 @@ package middleware import ( "ai-gateway/internal/models" "errors" - "log" "net/http" "strings" + "time" "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" "gorm.io/gorm" ) +var jwtSecret = []byte("your-secret-key") + // AuthMiddleware 创建并返回一个API密钥鉴权中间件 -func AuthMiddleware(db *gorm.DB) gin.HandlerFunc { +func AuthMiddleware(db *gorm.DB, webUIPassword string) gin.HandlerFunc { return func(c *gin.Context) { - // 从请求头获取 Authorization 值 authHeader := c.GetHeader("Authorization") - - // 检查是否为空或不以 "Bearer " 开头 if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") { - log.Printf("[Auth] Failed: Missing or invalid Authorization header. IP: %s", c.ClientIP()) - c.JSON(http.StatusUnauthorized, gin.H{ - "error": gin.H{ - "message": "Missing or invalid Authorization header", - "type": "authentication_error", - }, - }) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing or invalid Authorization header"}) c.Abort() return } - // 提取 Bearer token - token := strings.TrimPrefix(authHeader, "Bearer ") - if token == "" { - log.Printf("[Auth] Failed: Missing API key. IP: %s", c.ClientIP()) - c.JSON(http.StatusUnauthorized, gin.H{ - "error": gin.H{ - "message": "Missing API key", - "type": "authentication_error", - }, - }) - c.Abort() - return - } + tokenString := strings.TrimPrefix(authHeader, "Bearer ") - // 在数据库中查询API密钥 - var apiKey models.APIKey - if err := db.Where("key = ?", token).First(&apiKey).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - log.Printf("[Auth] Failed: Invalid API key. Key: %s, IP: %s", token, c.ClientIP()) - c.JSON(http.StatusUnauthorized, gin.H{ - "error": gin.H{ - "message": "Invalid API key", - "type": "authentication_error", - }, - }) - } else { - log.Printf("[Auth] Failed: Database error during authentication. Key: %s, IP: %s, Error: %v", token, c.ClientIP(), err) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": gin.H{ - "message": "Failed to authenticate", - "type": "internal_error", - }, - }) + // 检查是否为 Web UI 路由 + if strings.HasPrefix(c.Request.URL.Path, "/api") { + // Web UI 认证 (JWT) + claims := &jwt.RegisteredClaims{} + token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + return jwtSecret, nil + }) + + if err != nil || !token.Valid { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) + c.Abort() + return } - c.Abort() - return + } else { + // API 密钥认证 + var apiKey models.APIKey + if err := db.Where("key = ?", tokenString).First(&apiKey).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid API key"}) + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) + } + c.Abort() + return + } + c.Set("apiKey", apiKey) } - // 将API密钥对象存入上下文,供后续处理器使用 - c.Set("apiKey", apiKey) - - // 传递给下一个处理器 c.Next() } } + +func GenerateJWT() (string, error) { + claims := jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(jwtSecret) +} diff --git a/backend/main.go b/backend/main.go index 869bc3a..23d96a1 100644 --- a/backend/main.go +++ b/backend/main.go @@ -69,8 +69,9 @@ func main() { // 创建API处理器 handler := &api.APIHandler{ - DB: database, - LogCleaner: logCleaner, + DB: database, + LogCleaner: logCleaner, + WebUIPassword: cfg.App.WebUIPassword, } // 添加健康检查端点(无需认证) @@ -79,14 +80,14 @@ func main() { // 创建根组 root_ := router.Group("/") - root_.Use(middleware.AuthMiddleware(database)) + root_.Use(middleware.AuthMiddleware(database, cfg.App.WebUIPassword)) { root_.GET("/models", handler.ListModels) } // 创建受保护的路由组 protected := router.Group("/v1") - protected.Use(middleware.AuthMiddleware(database)) + protected.Use(middleware.AuthMiddleware(database, cfg.App.WebUIPassword)) { protected.GET("/models", handler.ListModels) protected.POST("/chat/completions", handler.ChatCompletions) @@ -96,7 +97,8 @@ func main() { // 创建API管理路由组 api_ := router.Group("/api") - api_.Use(middleware.AuthMiddleware(database)) + api_.POST("/login", handler.LoginHandler) + api_.Use(middleware.AuthMiddleware(database, cfg.App.WebUIPassword)) { // Providers api_.GET("/providers", handler.GetProvidersHandler) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e65c632..53c621d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,12 +1,31 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import PageLayout from './components/layout/PageLayout'; import ProvidersPage from './features/providers'; import VirtualModelsPage from './features/virtual-models'; import LogsPage from './features/logs'; import ApiKeysPage from './features/api-keys'; +import LoginPage from './components/LoginPage'; import './App.css'; function App() { + const [token, setToken] = useState(null); + + useEffect(() => { + const storedToken = localStorage.getItem('auth_token'); + if (storedToken) { + setToken(storedToken); + } + }, []); + + const handleLogin = (newToken) => { + localStorage.setItem('auth_token', newToken); + setToken(newToken); + }; + + if (!token) { + return ; + } + const tabs = [ { label: '提供商管理', diff --git a/frontend/src/components/LoginPage.jsx b/frontend/src/components/LoginPage.jsx new file mode 100644 index 0000000..aae8fdd --- /dev/null +++ b/frontend/src/components/LoginPage.jsx @@ -0,0 +1,48 @@ +import React, { useState } from 'react'; + +const LoginPage = ({ onLogin }) => { + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + + const handleSubmit = async (e) => { + e.preventDefault(); + try { + const response = await fetch('/api/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ password }), + }); + + if (response.ok) { + const data = await response.json(); + onLogin(data.token); + } else { + setError('密码错误'); + } + } catch (err) { + setError('登录失败,请稍后重试'); + } + }; + + return ( +
+
+

请输入密码访问

+ setPassword(e.target.value)} + style={{ padding: '8px', marginRight: '8px' }} + /> + + {error &&

{error}

} +
+
+ ); +}; + +export default LoginPage; \ No newline at end of file diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index 40495a0..4fe71de 100644 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -1,17 +1,14 @@ import axios from 'axios'; -// !!!重要!!! -// 请将这里的 'YOUR_API_KEY' 替换为您的真实 API 密钥 -const API_KEY = 'sk-dev-key-789012'; - const apiClient = axios.create({ baseURL: '/', }); apiClient.interceptors.request.use( (config) => { - if (API_KEY) { - config.headers.Authorization = `Bearer ${API_KEY}`; + const token = localStorage.getItem('auth_token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; } return config; },