调整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
|
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"]
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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(视为缓存未命中)。"""
|
||||||
|
try:
|
||||||
r = await get_redis()
|
r = await get_redis()
|
||||||
raw = await r.get(key)
|
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 不可用时静默跳过。"""
|
||||||
|
try:
|
||||||
r = await get_redis()
|
r = await get_redis()
|
||||||
await r.setex(key, ttl, json.dumps(value))
|
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
|
||||||
|
try:
|
||||||
r = await get_redis()
|
r = await get_redis()
|
||||||
await r.delete(*keys)
|
await r.delete(*keys)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -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,8 +77,7 @@ 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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -57,9 +57,14 @@ export default function Services() {
|
|||||||
okType: 'danger',
|
okType: 'danger',
|
||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
|
try {
|
||||||
await revokeService(id);
|
await revokeService(id);
|
||||||
message.success('已吊销');
|
message.success('已吊销');
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.response?.data?.detail || '吊销失败');
|
||||||
|
} finally {
|
||||||
load();
|
load();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -78,9 +83,14 @@ export default function Services() {
|
|||||||
okType: 'danger',
|
okType: 'danger',
|
||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
|
try {
|
||||||
await deleteService(id);
|
await deleteService(id);
|
||||||
message.success('已删除');
|
message.success('已删除');
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.response?.data?.detail || '删除失败');
|
||||||
|
} finally {
|
||||||
load();
|
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',
|
||||||
|
|||||||
@@ -72,9 +72,14 @@ export default function Tokens() {
|
|||||||
okType: 'danger',
|
okType: 'danger',
|
||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
|
try {
|
||||||
await revokeToken(id, '管理员手动吊销');
|
await revokeToken(id, '管理员手动吊销');
|
||||||
message.success('已吊销');
|
message.success('已吊销');
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.response?.data?.detail || '吊销失败');
|
||||||
|
} finally {
|
||||||
load();
|
load();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -93,9 +98,14 @@ export default function Tokens() {
|
|||||||
okType: 'danger',
|
okType: 'danger',
|
||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
|
try {
|
||||||
await deleteToken(id);
|
await deleteToken(id);
|
||||||
message.success('已删除');
|
message.success('已删除');
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.response?.data?.detail || '删除失败');
|
||||||
|
} finally {
|
||||||
load();
|
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="过期时间(留空=永不过期)">
|
||||||
|
|||||||
Reference in New Issue
Block a user