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