新增前端页面密码登录

This commit is contained in:
2025-11-17 11:42:17 +08:00
parent d1ebec2103
commit edb63eb7a3
9 changed files with 154 additions and 63 deletions

View File

@@ -2,6 +2,7 @@ package api
import (
"ai-gateway/internal/billing"
"ai-gateway/internal/middleware"
"ai-gateway/internal/models"
"ai-gateway/internal/scheduler"
"bytes"
@@ -18,6 +19,7 @@ import (
type APIHandler struct {
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})
}

View File

@@ -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

View File

@@ -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=

View File

@@ -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)
}

View File

@@ -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",
},
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
// 检查是否为 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
}
// 在数据库中查询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",
},
})
// 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
}
// 将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)
}

View File

@@ -71,6 +71,7 @@ func main() {
handler := &api.APIHandler{
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)

View File

@@ -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 <LoginPage onLogin={handleLogin} />;
}
const tabs = [
{
label: '提供商管理',

View File

@@ -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 (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<form onSubmit={handleSubmit}>
<h2>请输入密码访问</h2>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
style={{ padding: '8px', marginRight: '8px' }}
/>
<button type="submit" style={{ padding: '8px' }}>
登录
</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</form>
</div>
);
};
export default LoginPage;

View File

@@ -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;
},