调整签发Token逻辑

This commit is contained in:
2026-09-01 17:53:52 +08:00
parent 1f056ae099
commit 2ac36d7c72
4 changed files with 58 additions and 22 deletions
+8 -7
View File
@@ -35,6 +35,7 @@ def _row_to_dict(row) -> dict:
return {
"token_id": row["token_id"],
"token_prefix": row["token_prefix"],
"token_plain": row["token_plain"],
"client_id": row["client_id"],
"service_scope": row["service_scope"],
"status": row["status"],
@@ -85,13 +86,13 @@ async def create_token(req: TokenCreate, admin: dict = Depends(current_admin)):
if not exists:
raise HTTPException(400, f"service_scope '{req.service_scope}' 不是已注册的活跃服务")
# client_id 唯一校验:同一 client_id 不允许重复签发
# client_id + service_scope 唯一校验:同一客户端同一服务不允许重复签发
existing = await pool.fetchval(
"SELECT 1 FROM mcp_token WHERE client_id = $1 AND status = 'active'",
req.client_id,
"SELECT 1 FROM mcp_token WHERE client_id = $1 AND service_scope = $2 AND status = 'active'",
req.client_id, req.service_scope,
)
if existing:
raise HTTPException(400, f"client_id '{req.client_id}' 已存在活跃 Token,请先吊销旧 Token")
raise HTTPException(400, f"client_id '{req.client_id}' 已存在服务于 '{req.service_scope}' 的活跃 Token,请先吊销旧 Token")
# 生成明文 token:仅此一次返回
plain = secrets.token_urlsafe(32)
@@ -99,12 +100,12 @@ async def create_token(req: TokenCreate, admin: dict = Depends(current_admin)):
token_prefix = plain[:12] + "…"
row = await pool.fetchrow(
"""INSERT INTO mcp_token (token_hash, token_prefix, client_id, service_scope, status,
"""INSERT INTO mcp_token (token_hash, token_plain, token_prefix, client_id, service_scope, status,
expires_at, description, created_by)
VALUES ($1, $2, $3, $4, 'active', $5, $6, $7)
VALUES ($1, $2, $3, $4, $5, 'active', $6, $7, $8)
RETURNING token_id, token_prefix, client_id, service_scope, status,
expires_at, description, created_at, created_by""",
token_hash, token_prefix, req.client_id, req.service_scope,
token_hash, plain, token_prefix, req.client_id, req.service_scope,
req.expires_at, req.description, admin.get("username", "admin"),
)
+1
View File
@@ -3,6 +3,7 @@ import api from './client';
export interface TokenRow {
token_id: number;
token_prefix: string;
token_plain: string | null;
client_id: string;
service_scope: string;
status: string;
+46 -13
View File
@@ -11,7 +11,7 @@ import {
Typography,
message,
} from 'antd';
import { PlusOutlined, ReloadOutlined } from '@ant-design/icons';
import { PlusOutlined, ReloadOutlined, CopyOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import type { ColumnsType } from 'antd/es/table';
import {
@@ -110,22 +110,37 @@ export default function Tokens() {
});
};
const onCreate = async () => {
const values = await form.validateFields();
const res = await createToken({
client_id: values.client_id,
service_scope: values.service_scope,
description: values.description,
expires_at: values.expires_at ? values.expires_at.toISOString() : null,
const onCopy = (text: string) => {
navigator.clipboard.writeText(text).then(() => {
message.success('已复制');
});
setCreated(res);
setCreateOpen(false);
form.resetFields();
load();
};
const onCreate = async () => {
try {
const values = await form.validateFields();
const res = await createToken({
client_id: values.client_id,
service_scope: values.service_scope,
description: values.description,
expires_at: values.expires_at ? values.expires_at.toISOString() : null,
});
setCreated(res);
setCreateOpen(false);
form.resetFields();
load();
} catch (e: any) {
if (e?.response?.data?.detail) {
message.error(e.response.data.detail);
} else if (e?.errorFields) {
// 表单校验错误,antd 自动处理
} else {
message.error('签发失败');
}
}
};
const columns: ColumnsType<TokenRow> = [
{ title: '前缀', dataIndex: 'token_prefix', key: 'token_prefix', width: 140 },
{ title: '客户端', dataIndex: 'client_id', key: 'client_id', width: 240 },
{
title: '范围',
@@ -134,6 +149,24 @@ export default function Tokens() {
width: 80,
render: (s: string) => <Tag color="blue">{s}</Tag>,
},
{
title: 'Token',
dataIndex: 'token_plain',
key: 'token',
width: 300,
render: (plain: string | null, row: TokenRow) => {
if (!plain) return row.token_prefix;
const masked = plain.slice(0, 12) + '••••••••';
return (
<Space size="small">
<Typography.Text style={{ fontFamily: 'monospace', fontSize: 13 }} ellipsis>
{masked}
</Typography.Text>
<Button type="text" size="small" icon={<CopyOutlined />} onClick={() => onCopy(plain)} />
</Space>
);
},
},
{
title: '状态',
dataIndex: 'status',
+3 -2
View File
@@ -10,10 +10,11 @@
-- 1. mcp_token:token 主表
CREATE TABLE IF NOT EXISTS mcp_token (
token_id BIGSERIAL PRIMARY KEY,
token_hash VARCHAR(64) UNIQUE NOT NULL, -- sha256(明文),不存明文
token_hash VARCHAR(64) UNIQUE NOT NULL, -- sha256(明文),verify 校验用
token_plain VARCHAR(128), -- 明文 Token(管理后台二次复制用)
token_prefix VARCHAR(16) NOT NULL, -- 明文前 12 字符 + '…',前端识别用
client_id VARCHAR(64) NOT NULL, -- 调用方标识(如 trae / partner-a)
service_scope VARCHAR(32) NOT NULL, -- 'erp' | 'crm' | 'both'
service_scope VARCHAR(32) NOT NULL, -- 'erp' | 'crm'
status VARCHAR(16) NOT NULL DEFAULT 'active', -- active/revoked/expired
expires_at TIMESTAMPTZ, -- null = 永不过期
description VARCHAR(200), -- 用途说明