调整mcp token 签发逻辑

This commit is contained in:
2026-09-01 17:35:52 +08:00
parent a1f9942ca4
commit 1f056ae099
7 changed files with 77 additions and 47 deletions
+2 -2
View File
@@ -25,7 +25,7 @@ COPY backend/app ./app
COPY --from=frontend-build /app/frontend/dist ./app/static COPY --from=frontend-build /app/frontend/dist ./app/static
WORKDIR /app WORKDIR /app
EXPOSE 8000 EXPOSE 9000
# STATIC_DIR 指向后端 app/static(相对 backend 启动目录) # STATIC_DIR 指向后端 app/static(相对 backend 启动目录)
ENV STATIC_DIR=/app/app/static ENV STATIC_DIR=/app/app/static
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "9000"]
+2 -2
View File
@@ -4,7 +4,7 @@ import os
class Settings: class Settings:
AUTH_DB_HOST: str = os.getenv("AUTH_DB_HOST", "127.0.0.1") AUTH_DB_HOST: str = os.getenv("AUTH_DB_HOST", "47.101.220.40")
AUTH_DB_PORT: int = int(os.getenv("AUTH_DB_PORT", "5432")) AUTH_DB_PORT: int = int(os.getenv("AUTH_DB_PORT", "5432"))
AUTH_DB_USER: str = os.getenv("AUTH_DB_USER", "postgres") AUTH_DB_USER: str = os.getenv("AUTH_DB_USER", "postgres")
AUTH_DB_PASSWORD: str = os.getenv("AUTH_DB_PASSWORD", "digiwin") AUTH_DB_PASSWORD: str = os.getenv("AUTH_DB_PASSWORD", "digiwin")
@@ -24,7 +24,7 @@ class Settings:
STATIC_DIR: str = os.getenv("STATIC_DIR", "../frontend/dist") STATIC_DIR: str = os.getenv("STATIC_DIR", "../frontend/dist")
# Redis 配置 # Redis 配置
REDIS_HOST: str = os.getenv("REDIS_HOST", "127.0.0.1") REDIS_HOST: str = os.getenv("REDIS_HOST", "47.101.220.40")
REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379")) REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379"))
REDIS_DB: int = int(os.getenv("REDIS_DB", "0")) REDIS_DB: int = int(os.getenv("REDIS_DB", "0"))
REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "digiwin") REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "digiwin")
+18 -9
View File
@@ -39,9 +39,12 @@ async def close_redis() -> None:
async def cache_get(key: str) -> dict | None: async def cache_get(key: str) -> dict | None:
"""读取 JSON 缓存,返回 dict 或 None。""" """读取 JSON 缓存,返回 dict 或 None。Redis 不可用时返回 None(视为缓存未命中)。"""
r = await get_redis() try:
raw = await r.get(key) r = await get_redis()
raw = await r.get(key)
except Exception:
return None
if raw is None: if raw is None:
return None return None
try: try:
@@ -51,14 +54,20 @@ async def cache_get(key: str) -> dict | None:
async def cache_set(key: str, value: dict, ttl: int) -> None: async def cache_set(key: str, value: dict, ttl: int) -> None:
"""写入 JSON 缓存,带 TTL(秒)。""" """写入 JSON 缓存,带 TTL(秒)。Redis 不可用时静默跳过。"""
r = await get_redis() try:
await r.setex(key, ttl, json.dumps(value)) r = await get_redis()
await r.setex(key, ttl, json.dumps(value))
except Exception:
pass
async def cache_delete(*keys: str) -> None: async def cache_delete(*keys: str) -> None:
"""删除缓存 key。""" """删除缓存 key。Redis 不可用时静默跳过,缓存会自然过期。"""
if not keys: if not keys:
return return
r = await get_redis() try:
await r.delete(*keys) r = await get_redis()
await r.delete(*keys)
except Exception:
pass
+8 -9
View File
@@ -17,7 +17,7 @@ router = APIRouter(prefix="/api/tokens", tags=["tokens"])
class TokenCreate(BaseModel): class TokenCreate(BaseModel):
client_id: str client_id: str
service_scope: str = "both" service_scope: str
description: str | None = None description: str | None = None
expires_at: datetime | None = None # null = 永不过期 expires_at: datetime | None = None # null = 永不过期
@@ -77,14 +77,13 @@ async def list_tokens(
async def create_token(req: TokenCreate, admin: dict = Depends(current_admin)): async def create_token(req: TokenCreate, admin: dict = Depends(current_admin)):
pool = await get_pool() pool = await get_pool()
# 动态校验 service_scope:both 始终有效,其他值必须是已注册的 active 服务 # 动态校验 service_scope:必须是已注册的 active 服务
if req.service_scope != "both": exists = await pool.fetchval(
exists = await pool.fetchval( "SELECT 1 FROM mcp_service WHERE service_name = $1 AND status = 'active'",
"SELECT 1 FROM mcp_service WHERE service_name = $1 AND status = 'active'", req.service_scope,
req.service_scope, )
) if not exists:
if not exists: raise HTTPException(400, f"service_scope '{req.service_scope}' 不是已注册的活跃服务")
raise HTTPException(400, f"service_scope '{req.service_scope}' 不是已注册的活跃服务")
# client_id 唯一校验:同一 client_id 不允许重复签发 # client_id 唯一校验:同一 client_id 不允许重复签发
existing = await pool.fetchval( existing = await pool.fetchval(
+3 -3
View File
@@ -108,7 +108,7 @@ async def verify_token(req: VerifyReq, service: str = Depends(_resolve_service))
return VerifyResp(valid=False) return VerifyResp(valid=False)
# 校验服务范围 # 校验服务范围
scope = cached.get("service_scope") scope = cached.get("service_scope")
if scope != "both" and scope != service: if scope != service:
return VerifyResp(valid=False) return VerifyResp(valid=False)
# 异步更新 last_used # 异步更新 last_used
pool = await get_pool() pool = await get_pool()
@@ -146,9 +146,9 @@ async def verify_token(req: VerifyReq, service: str = Depends(_resolve_service))
if row["expires_at"] is not None and row["expires_at"].timestamp() < datetime.now(timezone.utc).timestamp(): if row["expires_at"] is not None and row["expires_at"].timestamp() < datetime.now(timezone.utc).timestamp():
return VerifyResp(valid=False) return VerifyResp(valid=False)
# 服务范围校验:both 放行所有;否则要求精确匹配 # 服务范围校验:要求精确匹配
scope = row["service_scope"] scope = row["service_scope"]
if scope != "both" and scope != service: if scope != service:
return VerifyResp(valid=False) return VerifyResp(valid=False)
# 异步更新 last_used_at / last_used_svc # 异步更新 last_used_at / last_used_svc
+17 -7
View File
@@ -57,9 +57,14 @@ export default function Services() {
okType: 'danger', okType: 'danger',
cancelText: '取消', cancelText: '取消',
onOk: async () => { onOk: async () => {
await revokeService(id); try {
message.success('已吊销'); await revokeService(id);
load(); message.success('已吊销');
} catch (e: any) {
message.error(e?.response?.data?.detail || '吊销失败');
} finally {
load();
}
}, },
}); });
}; };
@@ -78,9 +83,14 @@ export default function Services() {
okType: 'danger', okType: 'danger',
cancelText: '取消', cancelText: '取消',
onOk: async () => { onOk: async () => {
await deleteService(id); try {
message.success('已删除'); await deleteService(id);
load(); message.success('已删除');
} catch (e: any) {
message.error(e?.response?.data?.detail || '删除失败');
} finally {
load();
}
}, },
}); });
}; };
@@ -104,7 +114,7 @@ export default function Services() {
}; };
const columns: ColumnsType<ServiceRow> = [ const columns: ColumnsType<ServiceRow> = [
{ title: '服务名', dataIndex: 'service_name', key: 'service_name', width: 120 }, { title: '服务名', dataIndex: 'service_name', key: 'service_name', width: 240 },
{ {
title: 'API Key', title: 'API Key',
dataIndex: 'api_key', dataIndex: 'api_key',
+27 -15
View File
@@ -72,9 +72,14 @@ export default function Tokens() {
okType: 'danger', okType: 'danger',
cancelText: '取消', cancelText: '取消',
onOk: async () => { onOk: async () => {
await revokeToken(id, '管理员手动吊销'); try {
message.success('已吊销'); await revokeToken(id, '管理员手动吊销');
load(); message.success('已吊销');
} catch (e: any) {
message.error(e?.response?.data?.detail || '吊销失败');
} finally {
load();
}
}, },
}); });
}; };
@@ -93,9 +98,14 @@ export default function Tokens() {
okType: 'danger', okType: 'danger',
cancelText: '取消', cancelText: '取消',
onOk: async () => { onOk: async () => {
await deleteToken(id); try {
message.success('已删除'); await deleteToken(id);
load(); message.success('已删除');
} catch (e: any) {
message.error(e?.response?.data?.detail || '删除失败');
} finally {
load();
}
}, },
}); });
}; };
@@ -204,7 +214,7 @@ export default function Tokens() {
okText="签发" okText="签发"
cancelText="取消" cancelText="取消"
> >
<Form form={form} layout="vertical" initialValues={{ service_scope: 'both' }}> <Form form={form} layout="vertical" initialValues={{ service_scope: undefined }}>
<Form.Item <Form.Item
name="client_id" name="client_id"
label="客户端标识" label="客户端标识"
@@ -212,15 +222,17 @@ export default function Tokens() {
> >
<Input placeholder="如 trae / partner-a" /> <Input placeholder="如 trae / partner-a" />
</Form.Item> </Form.Item>
<Form.Item name="service_scope" label="服务范围"> <Form.Item
name="service_scope"
label="服务范围"
rules={[{ required: true, message: '请选择服务' }]}
>
<Select <Select
options={[ placeholder="选择 MCP 服务"
{ value: 'both', label: '全部服务' }, options={services.map((s) => ({
...services.map((s) => ({ value: s.service_name,
value: s.service_name, label: s.service_name.toUpperCase(),
label: `仅 ${s.service_name.toUpperCase()}`, }))}
})),
]}
/> />
</Form.Item> </Form.Item>
<Form.Item name="expires_at" label="过期时间(留空=永不过期)"> <Form.Item name="expires_at" label="过期时间(留空=永不过期)">