调整mcp token 签发逻辑
This commit is contained in:
+2
-2
@@ -25,7 +25,7 @@ COPY backend/app ./app
|
||||
COPY --from=frontend-build /app/frontend/dist ./app/static
|
||||
|
||||
WORKDIR /app
|
||||
EXPOSE 8000
|
||||
EXPOSE 9000
|
||||
# STATIC_DIR 指向后端 app/static(相对 backend 启动目录)
|
||||
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"]
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
|
||||
|
||||
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_USER: str = os.getenv("AUTH_DB_USER", "postgres")
|
||||
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")
|
||||
|
||||
# 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_DB: int = int(os.getenv("REDIS_DB", "0"))
|
||||
REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "digiwin")
|
||||
|
||||
@@ -39,9 +39,12 @@ async def close_redis() -> None:
|
||||
|
||||
|
||||
async def cache_get(key: str) -> dict | None:
|
||||
"""读取 JSON 缓存,返回 dict 或 None。"""
|
||||
"""读取 JSON 缓存,返回 dict 或 None。Redis 不可用时返回 None(视为缓存未命中)。"""
|
||||
try:
|
||||
r = await get_redis()
|
||||
raw = await r.get(key)
|
||||
except Exception:
|
||||
return None
|
||||
if raw is None:
|
||||
return None
|
||||
try:
|
||||
@@ -51,14 +54,20 @@ async def cache_get(key: str) -> dict | None:
|
||||
|
||||
|
||||
async def cache_set(key: str, value: dict, ttl: int) -> None:
|
||||
"""写入 JSON 缓存,带 TTL(秒)。"""
|
||||
"""写入 JSON 缓存,带 TTL(秒)。Redis 不可用时静默跳过。"""
|
||||
try:
|
||||
r = await get_redis()
|
||||
await r.setex(key, ttl, json.dumps(value))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def cache_delete(*keys: str) -> None:
|
||||
"""删除缓存 key。"""
|
||||
"""删除缓存 key。Redis 不可用时静默跳过,缓存会自然过期。"""
|
||||
if not keys:
|
||||
return
|
||||
try:
|
||||
r = await get_redis()
|
||||
await r.delete(*keys)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -17,7 +17,7 @@ router = APIRouter(prefix="/api/tokens", tags=["tokens"])
|
||||
|
||||
class TokenCreate(BaseModel):
|
||||
client_id: str
|
||||
service_scope: str = "both"
|
||||
service_scope: str
|
||||
description: str | None = None
|
||||
expires_at: datetime | None = None # null = 永不过期
|
||||
|
||||
@@ -77,8 +77,7 @@ async def list_tokens(
|
||||
async def create_token(req: TokenCreate, admin: dict = Depends(current_admin)):
|
||||
pool = await get_pool()
|
||||
|
||||
# 动态校验 service_scope:both 始终有效,其他值必须是已注册的 active 服务
|
||||
if req.service_scope != "both":
|
||||
# 动态校验 service_scope:必须是已注册的 active 服务
|
||||
exists = await pool.fetchval(
|
||||
"SELECT 1 FROM mcp_service WHERE service_name = $1 AND status = 'active'",
|
||||
req.service_scope,
|
||||
|
||||
@@ -108,7 +108,7 @@ async def verify_token(req: VerifyReq, service: str = Depends(_resolve_service))
|
||||
return VerifyResp(valid=False)
|
||||
# 校验服务范围
|
||||
scope = cached.get("service_scope")
|
||||
if scope != "both" and scope != service:
|
||||
if scope != service:
|
||||
return VerifyResp(valid=False)
|
||||
# 异步更新 last_used
|
||||
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():
|
||||
return VerifyResp(valid=False)
|
||||
|
||||
# 服务范围校验:both 放行所有;否则要求精确匹配
|
||||
# 服务范围校验:要求精确匹配
|
||||
scope = row["service_scope"]
|
||||
if scope != "both" and scope != service:
|
||||
if scope != service:
|
||||
return VerifyResp(valid=False)
|
||||
|
||||
# 异步更新 last_used_at / last_used_svc
|
||||
|
||||
@@ -57,9 +57,14 @@ export default function Services() {
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await revokeService(id);
|
||||
message.success('已吊销');
|
||||
} catch (e: any) {
|
||||
message.error(e?.response?.data?.detail || '吊销失败');
|
||||
} finally {
|
||||
load();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -78,9 +83,14 @@ export default function Services() {
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteService(id);
|
||||
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> = [
|
||||
{ title: '服务名', dataIndex: 'service_name', key: 'service_name', width: 120 },
|
||||
{ title: '服务名', dataIndex: 'service_name', key: 'service_name', width: 240 },
|
||||
{
|
||||
title: 'API Key',
|
||||
dataIndex: 'api_key',
|
||||
|
||||
@@ -72,9 +72,14 @@ export default function Tokens() {
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await revokeToken(id, '管理员手动吊销');
|
||||
message.success('已吊销');
|
||||
} catch (e: any) {
|
||||
message.error(e?.response?.data?.detail || '吊销失败');
|
||||
} finally {
|
||||
load();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -93,9 +98,14 @@ export default function Tokens() {
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteToken(id);
|
||||
message.success('已删除');
|
||||
} catch (e: any) {
|
||||
message.error(e?.response?.data?.detail || '删除失败');
|
||||
} finally {
|
||||
load();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -204,7 +214,7 @@ export default function Tokens() {
|
||||
okText="签发"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Form form={form} layout="vertical" initialValues={{ service_scope: 'both' }}>
|
||||
<Form form={form} layout="vertical" initialValues={{ service_scope: undefined }}>
|
||||
<Form.Item
|
||||
name="client_id"
|
||||
label="客户端标识"
|
||||
@@ -212,15 +222,17 @@ export default function Tokens() {
|
||||
>
|
||||
<Input placeholder="如 trae / partner-a" />
|
||||
</Form.Item>
|
||||
<Form.Item name="service_scope" label="服务范围">
|
||||
<Form.Item
|
||||
name="service_scope"
|
||||
label="服务范围"
|
||||
rules={[{ required: true, message: '请选择服务' }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'both', label: '全部服务' },
|
||||
...services.map((s) => ({
|
||||
placeholder="选择 MCP 服务"
|
||||
options={services.map((s) => ({
|
||||
value: s.service_name,
|
||||
label: `仅 ${s.service_name.toUpperCase()}`,
|
||||
})),
|
||||
]}
|
||||
label: s.service_name.toUpperCase(),
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="expires_at" label="过期时间(留空=永不过期)">
|
||||
|
||||
Reference in New Issue
Block a user