fix: 修复token验证问题,租户管理API存在401未授权问题需要进一步排查

This commit is contained in:
Xin 2025-05-07 19:08:49 +08:00
parent 802c57003c
commit b5fa1b0d63
11 changed files with 631 additions and 69 deletions

View File

@ -1,9 +1,10 @@
import uuid
from datetime import datetime, timedelta, timezone
from fastapi import FastAPI, Depends, HTTPException, status, Request, Body, Response
from fastapi import FastAPI, Depends, HTTPException, status, Request, Body, Response, UploadFile, File
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from models import AccountCreate, AccountResponse, PasswordChange, TenantCreate, TenantResponse
from models import AccountCreate, AccountResponse, PasswordChange, TenantCreate, TenantResponse, ModelConfig
from model_manager import ModelManager
from account_manager import AccountManager as DifyAccountManager
from backend_account_manager import BackendAccountManager
@ -48,11 +49,12 @@ except Exception as e:
# 添加CORS中间件
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_origins=["http://localhost:3000", "http://127.0.0.1:3000", "http://localhost:3001", "http://127.0.0.1:3001"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["*"]
expose_headers=["*"],
max_age=600
)
api_auth = APIAuthManager()
op_logger = OperationLogger()
@ -111,6 +113,9 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
headers={"WWW-Authenticate": "Bearer"},
)
try:
# 打印接收到的token用于调试
logger.info(f"Received token: {token[:10]}...{token[-10:]}")
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
@ -122,11 +127,18 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
if not user:
raise credentials_exception
logger.info(f"Authenticated user: {username}")
return {
"id": str(user.get("id")),
"username": user.get("username"),
"email": user.get("email", "")
}
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token已过期",
headers={"WWW-Authenticate": "Bearer"},
)
except JWTError:
raise credentials_exception
@ -176,16 +188,27 @@ async def auth_register(request: Request):
)
@app.post("/api/auth/login")
async def auth_login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()):
async def auth_login(request: Request):
"""用户登录(auth)"""
try:
data = await request.json()
username = data.get("username")
password = data.get("password")
if not username or not password:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="缺少用户名或密码"
)
client_ip = request.client.host if request.client else "unknown"
user = backend_account_manager.get_user_by_username(form_data.username)
if not user or not backend_account_manager.verify_password(form_data.password, user["password"], user["password_salt"]):
user = backend_account_manager.get_user_by_username(username)
if not user or not backend_account_manager.verify_password(password, user["password"], user["password_salt"]):
op_logger.log_operation(
user_id=0,
operation_type="LOGIN_ATTEMPT",
endpoint="/api/auth/login",
parameters=f"username={form_data.username}, ip={client_ip}",
parameters=f"username={username}, ip={client_ip}",
status="FAILED"
)
raise HTTPException(
@ -193,6 +216,7 @@ async def auth_login(request: Request, form_data: OAuth2PasswordRequestForm = De
detail="用户名或密码错误",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user["username"]},
@ -206,11 +230,17 @@ async def auth_login(request: Request, form_data: OAuth2PasswordRequestForm = De
status="SUCCESS"
)
return {"access_token": access_token, "token_type": "bearer"}
except Exception as e:
logger.error(f"登录失败: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="登录失败"
)
@app.post("/api/user/login")
async def user_login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()):
async def user_login(request: Request):
"""用户登录(user)"""
return await auth_login(request, form_data)
return await auth_login(request)
@app.post("/api/auth/refresh")
async def refresh_token(current_user: dict = Depends(get_current_user)):
@ -438,6 +468,154 @@ async def get_tenant(name: str, current_user: dict = Depends(get_current_user)):
)
raise HTTPException(status_code=400, detail="查询租户失败")
# 模型管理路由组
@app.post("/api/models/upload")
async def upload_model_config(
file: UploadFile = File(...),
current_user: dict = Depends(get_current_user)
):
"""上传模型配置文件"""
try:
# 检查是否为admin用户
if current_user["username"] != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="需要admin用户权限"
)
# 读取上传的文件内容
contents = await file.read()
config = json.loads(contents)
# 验证模型配置
required_fields = ["model_name", "provider_name", "model_type", "api_key"]
for field in required_fields:
if field not in config:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"模型配置缺少必要字段: {field}"
)
return {"message": "模型配置上传成功", "config": config}
except json.JSONDecodeError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="无效的JSON格式"
)
except Exception as e:
logger.error(f"上传模型配置失败: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="上传模型配置失败"
)
@app.post("/api/models/assign")
async def assign_model_to_tenant(
model_config: ModelConfig,
tenant_id: Optional[str] = None,
current_user: dict = Depends(get_current_user)
):
"""分配模型给租户"""
try:
# 检查是否为admin用户
if current_user["username"] != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="需要admin用户权限"
)
# 如果未指定租户ID则为所有租户添加模型
if not tenant_id:
total_added = ModelManager.add_models_for_all_tenants()
return {"message": f"模型已成功分配给所有租户,共{total_added}"}
# 为指定租户添加模型
tenant = TenantManager.get_tenant_by_id(tenant_id)
if not tenant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="租户不存在"
)
ModelManager.add_model_for_tenant(tenant_id, tenant["encrypt_public_key"], model_config.dict())
return {"message": "模型已成功分配给租户"}
except Exception as e:
logger.error(f"分配模型失败: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="分配模型失败"
)
@app.get("/api/models/{tenant_id}")
async def get_tenant_models(
tenant_id: str,
current_user: dict = Depends(get_current_user)
):
"""获取租户的模型列表"""
try:
models = ModelManager.get_tenant_models(tenant_id)
return {
"tenant_id": tenant_id,
"models": models
}
except Exception as e:
logger.error(f"获取租户模型失败: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="获取租户模型失败"
)
@app.delete("/api/models/{model_id}")
async def delete_model(
model_id: str,
current_user: dict = Depends(get_current_user)
):
"""删除特定模型"""
try:
# 检查是否为admin用户
if current_user["username"] != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="需要admin用户权限"
)
rows_affected = ModelManager.delete_model(model_id)
if rows_affected == 0:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="模型不存在"
)
return {"message": "模型删除成功"}
except Exception as e:
logger.error(f"删除模型失败: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="删除模型失败"
)
@app.delete("/api/models/all/{model_name}")
async def delete_model_for_all_tenants(
model_name: str,
current_user: dict = Depends(get_current_user)
):
"""删除所有租户的特定模型"""
try:
# 检查管理员权限
if current_user["username"] != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="需要管理员权限"
)
total_deleted = ModelManager.delete_specific_model_for_all_tenants(model_name)
return {"message": f"模型已从所有租户中删除,共{total_deleted}"}
except Exception as e:
logger.error(f"批量删除模型失败: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="批量删除模型失败"
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8001)

View File

@ -14,31 +14,67 @@ class ModelManager:
"""模型管理类"""
@staticmethod
def load_config(config_path):
"""加载模型配置文件"""
def add_volc_models_for_tenant(tenant_name, config_path=CONFIG_PATHS['volc_model_config']):
"""为指定租户添加火山模型"""
try:
if not os.path.exists(config_path):
raise FileNotFoundError(f"配置文件 {config_path} 不存在!")
with open(config_path, "r", encoding="utf-8") as f:
config = json.load(f)
return config
# 加载模型配置
config = ModelManager.load_config(config_path)
models = config.get("models", [])
# 获取租户信息
tenant = TenantManager.get_tenant_by_name(tenant_name)
if not tenant:
logger.error(f"未找到租户: {tenant_name}")
return 0
# 为租户添加模型
total_added = 0
for model_config in models:
if ModelManager.add_volc_model_for_tenant(tenant['id'], tenant['encrypt_public_key'], model_config):
total_added += 1
logger.info(f"为租户 {tenant_name} 添加火山模型完成,共添加 {total_added} 条记录。")
return total_added
except Exception as e:
logger.error(f"加载配置文件失败: {e}")
logger.error(f"为租户 {tenant_name} 添加火山模型失败: {e}")
raise
@staticmethod
def check_model_exists(tenant_id, model_name):
"""检查指定租户是否已存在指定的模型"""
def get_tenant_models(tenant_id: str):
"""获取租户的模型列表"""
try:
query = """
SELECT COUNT(*) FROM provider_models
WHERE tenant_id = %s AND model_name = %s;
SELECT
id,
provider_name,
model_name,
model_type,
encrypted_config,
is_valid,
created_at,
updated_at
FROM provider_models
WHERE tenant_id = %s
ORDER BY created_at DESC;
"""
count = execute_query(query, (tenant_id, model_name), fetch_one=True)[0]
return count > 0
with get_db_cursor() as cursor:
cursor.execute(query, (tenant_id,))
models = cursor.fetchall()
return [{
"id": str(model[0]),
"provider_name": model[1],
"model_name": model[2],
"model_type": model[3],
"encrypted_config": json.loads(model[4]),
"is_valid": model[5],
"created_at": model[6],
"updated_at": model[7]
} for model in models]
except Exception as e:
logger.error(f"检查模型是否存在时发生错误: {e}")
return False
logger.error(f"获取租户模型失败: {e}")
raise
@staticmethod
def add_model_for_tenant(tenant_id, public_key_pem, model_config):

View File

@ -1,7 +1,32 @@
from pydantic import BaseModel, EmailStr
from typing import Optional
from typing import Optional, List
from datetime import datetime
class ModelConfig(BaseModel):
"""模型配置上传模型"""
model_name: str
provider_name: str
model_type: str
api_key: str
endpoint_url: Optional[str] = None
display_name: Optional[str] = None
context_size: Optional[int] = None
max_tokens_to_sample: Optional[int] = None
class ModelResponse(BaseModel):
"""模型响应模型"""
id: str
model_name: str
provider_name: str
model_type: str
created_at: datetime
class TenantModelResponse(BaseModel):
"""租户模型响应模型"""
tenant_id: str
tenant_name: str
models: List[ModelResponse]
class AccountCreate(BaseModel):
"""创建账户请求模型"""
username: str

View File

@ -1,13 +1,13 @@
import { request } from '../../axios/service'
import type { LoginParams, RegisterParams, LoginForm } from '@/api/auth/types'
export const login = (formData: FormData | LoginForm) =>
export const login = (data: { username: string; password: string }) =>
request<{ access_token: string }>({
method: 'POST',
url: '/api/auth/login',
data: formData,
data: data,
headers: {
'Content-Type': 'multipart/form-data'
'Content-Type': 'application/json'
}
})

View File

@ -0,0 +1,50 @@
import { request } from '@/axios/service'
import type { ApiResponse, ModelConfig, ModelResponse, TenantModelResponse } from './types.ts'
export function uploadModelConfig(file: File): Promise<ApiResponse<{config: any}>> {
const formData = new FormData()
formData.append('file', file)
return request({
url: '/api/models/upload',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
export function assignModelToTenant(
modelConfig: ModelConfig,
tenantId?: string
): Promise<ApiResponse<{message: string}>> {
return request({
url: '/api/models/assign',
method: 'post',
data: {
model_config: modelConfig,
tenant_id: tenantId
}
})
}
export function getTenantModels(tenantId: string): Promise<ApiResponse<TenantModelResponse>> {
return request({
url: `/api/models/${tenantId}`,
method: 'get'
})
}
export function deleteModel(modelId: string): Promise<ApiResponse<{message: string}>> {
return request({
url: `/api/models/${modelId}`,
method: 'delete'
})
}
export function deleteModelForAllTenants(modelName: string): Promise<ApiResponse<{message: string}>> {
return request({
url: `/api/models/all/${modelName}`,
method: 'delete'
})
}

View File

@ -0,0 +1,30 @@
export interface ApiResponse<T = any> {
code: number
message: string
data: T
}
export interface ModelConfig {
model_name: string
provider_name: string
model_type: string
api_key: string
endpoint_url?: string
display_name?: string
context_size?: number
max_tokens_to_sample?: number
}
export interface ModelResponse {
id: string
model_name: string
provider_name: string
model_type: string
created_at: string
}
export interface TenantModelResponse {
tenant_id: string
tenant_name: string
models: ModelResponse[]
}

View File

@ -10,8 +10,15 @@ export interface TenantForm {
description: string
}
export interface TenantListParams {
search?: string
page?: number
pageSize?: number
}
export interface TenantListResponse {
tenants: TenantItem[]
total?: number
}
export interface TenantDetailResponse {

View File

@ -7,10 +7,12 @@ const service = createAxios()
// 请求拦截器
service.interceptors.request.use(
(config) => {
const userStore = useUserStore()
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`
const token = localStorage.getItem('access_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
config.headers['Content-Type'] = 'application/json'
config.withCredentials = true
return config
},
(error) => {

View File

@ -32,7 +32,8 @@ const router = createRouter({
},
{
path: 'model',
component: () => import('../views/Model/index.vue')
component: () => import('../views/Model/index.vue'),
meta: { requiresAuth: true }
}
]
}

View File

@ -72,20 +72,20 @@ const loginRules = {
const loading = ref(false)
const loginFormRef = ref()
const handleLogin = async () => {
const handleLogin = async () => {
try {
loading.value = true
await loginFormRef.value.validate()
const encryptedPassword = await encryptPassword(loginForm.value.password)
const formData = new FormData()
formData.append('username', loginForm.value.username)
formData.append('password', encryptedPassword)
const res = await login(formData as LoginFormData)
const res = await login({
username: loginForm.value.username,
password: encryptedPassword
})
console.log('登录成功:', res)
// token
const token = res.access_token || res.token
const token = res.access_token
setToken(token)
localStorage.setItem('token', token)
setupTokenRefresh()

View File

@ -1,16 +1,249 @@
<template>
<div class="model-container">
<h1>模型管理</h1>
<!-- 模型列表和配置表单将在这里实现 -->
<el-card class="upload-card">
<el-upload
class="upload-demo"
drag
:auto-upload="false"
:on-change="handleFileChange"
accept=".json"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
拖拽模型配置文件到此处或<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
请上传JSON格式的模型配置文件
</div>
</template>
</el-upload>
<el-button
type="primary"
@click="uploadModel"
:disabled="!selectedFile"
>
上传模型
</el-button>
</el-card>
<el-card class="tenant-search-card">
<el-form :inline="true">
<el-form-item label="租户查询">
<el-input
v-model="searchQuery"
placeholder="输入租户名称或ID"
clearable
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="searchTenants">
查询
</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="tenant-list-card" v-if="searchedTenants.length > 0">
<el-table :data="searchedTenants" style="width: 100%">
<el-table-column prop="name" label="租户名称" />
<el-table-column prop="id" label="租户ID" />
<el-table-column label="操作">
<template #default="scope">
<el-button
size="small"
type="primary"
@click="showTenantModels(scope.row.id)"
>
查询模型
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="modelDialogVisible" title="租户模型信息">
<el-table :data="models" style="width: 100%">
<el-table-column prop="model_name" label="模型名称" />
<el-table-column prop="provider_name" label="提供商" />
<el-table-column prop="model_type" label="模型类型" />
<el-table-column prop="created_at" label="创建时间" />
</el-table>
</el-dialog>
<el-dialog v-model="assignDialogVisible" title="分配模型">
<el-form :model="assignForm">
<el-form-item label="选择租户">
<el-select v-model="assignForm.tenantId" placeholder="请选择租户">
<el-option
v-for="tenant in tenants"
:key="tenant.id"
:label="tenant.name"
:value="tenant.id"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="assignDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmAssign">
确认分配
</el-button>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
//
import { ref } from 'vue'
import { UploadFilled } from '@element-plus/icons-vue'
import {
uploadModelConfig,
assignModelToTenant,
getTenantModels,
deleteModel as deleteModelApi
} from '@/api/model'
import { fetchTenants } from '@/api/tenant'
import { ElMessage } from 'element-plus'
import type { ModelResponse } from '@/api/model/types'
interface Tenant {
id: string
name: string
description: string
createdAt: string
}
const selectedFile = ref<File | null>(null)
const models = ref<ModelResponse[]>([])
const tenants = ref<Tenant[]>([])
const searchedTenants = ref<Tenant[]>([])
const searchQuery = ref('')
const modelDialogVisible = ref(false)
const assignDialogVisible = ref(false)
const currentModel = ref<ModelResponse | null>(null)
const assignForm = ref({
tenantId: ''
})
const searchTenants = async () => {
try {
const res = await fetchTenants({ search: searchQuery.value })
searchedTenants.value = res.tenants
} catch (error) {
ElMessage.error('查询租户失败')
}
}
const showTenantModels = async (tenantId: string) => {
try {
const res = await getTenantModels(tenantId)
models.value = res.data.models
modelDialogVisible.value = true
} catch (error) {
ElMessage.error('加载模型列表失败')
}
}
const handleTenantChange = async (tenantId: string) => {
try {
const res = await getTenantModels(tenantId)
models.value = res.data.models
} catch (error) {
ElMessage.error('加载模型列表失败')
}
}
const handleFileChange = (file: File) => {
selectedFile.value = file
}
const uploadModel = async () => {
if (!selectedFile.value) return
try {
const res = await uploadModelConfig(selectedFile.value)
ElMessage.success(res.message)
loadModels()
selectedFile.value = null
} catch (error) {
ElMessage.error('上传失败')
}
}
const loadModels = async () => {
try {
// storeID
const currentTenantId = localStorage.getItem('currentTenantId')
if (!currentTenantId) {
ElMessage.warning('请先选择租户')
return
}
const res = await getTenantModels(currentTenantId)
models.value = res.data.models
} catch (error) {
ElMessage.error('加载模型列表失败')
}
}
const loadTenants = async () => {
try {
const res = await fetchTenants({})
tenants.value = res.tenants
} catch (error) {
ElMessage.error('加载租户列表失败')
}
}
const assignModel = (model: ModelResponse) => {
currentModel.value = model
assignDialogVisible.value = true
}
const confirmAssign = async () => {
if (!currentModel.value) return
try {
await assignModelToTenant({
model_name: currentModel.value.model_name,
provider_name: currentModel.value.provider_name,
model_type: currentModel.value.model_type,
api_key: '' //
}, assignForm.value.tenantId)
ElMessage.success('分配成功')
assignDialogVisible.value = false
} catch (error) {
ElMessage.error('分配失败')
}
}
const deleteModel = async (modelId: string) => {
try {
await deleteModelApi(modelId)
ElMessage.success('删除成功')
loadModels()
} catch (error) {
ElMessage.error('删除失败')
}
}
//
loadModels()
loadTenants()
</script>
<style scoped>
.model-container {
padding: 20px;
}
.upload-card {
margin-bottom: 20px;
}
.model-list-card {
margin-top: 20px;
}
</style>