新增前端页面密码登录
This commit is contained in:
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: '提供商管理',
|
||||
|
||||
48
frontend/src/components/LoginPage.jsx
Normal file
48
frontend/src/components/LoginPage.jsx
Normal 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;
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user