docs(mcp): 设计每用户 Token 接入方案

This commit is contained in:
xqzp2026
2026-09-04 17:40:57 +09:30
parent e331650dcd
commit 1bb115dee0
@@ -0,0 +1,282 @@
# xiaoqu-ai 每用户 MCP Token 接入开发设计文档
## 1. 需求概述
### 1.1 需求信息
| 项目 | 内容 |
|---|---|
| 需求标题 | xiaoqu-ai 每用户独立 MCP Token 接入 |
| 优先级 | high |
| 目标客户端 | xiaoqu-ai iOS ClawMobile |
| 目标服务 | xiaoqu-agent-service `/mcp` |
| 认证要求 | 每个登录用户使用自己的 MCP Token |
| 预估工时 | 12-16h |
### 1.2 成功标准
- 用户 A 的 MCP 请求只能携带用户 A 的 Token,不能使用用户 B 或应用公共 Token。
- 登录、刷新 Token、切换账号、退出登录后,Claw/MCP 会话不会串用旧用户凭证。
- MCP 请求使用 `Authorization: Bearer <token>`,服务端继续执行用户、租户、作用域和审计校验。
- Token 不出现在 prompt、工具参数、普通日志、崩溃信息或 Git 文件中。
- 用户无 MCP Token、Token 过期、Token 被撤销时,客户端显示可恢复错误,不发送无凭证请求。
### 1.3 明确不做
- 不把一个固定 Token 写入 App、公共配置文件或所有用户共用。
- 不把数据库中保存的 Token 哈希当作客户端 Token 返回;明文只在签发响应中出现一次。
- 不让客户端提交任意 `user_id`、`tenant_id` 来冒充用户身份。
## 2. 现状分析
### 2.1 相关现有代码
| 文件 | 层级 | 当前功能 | 改动类型 |
|---|---|---|---|
| `xiaoqu-ai/ios/Sources/XiaoquCRM/Core/Dependencies/ClawBridge.swift` | iOS 依赖 | 静态初始化 Claw 配置,并创建 MCP Server 列表 | 修改 |
| `xiaoqu-ai/claw-code-parity/rust/crates/claw-mobile/src/ffi.rs` | Rust FFI | 接收 MCP 配置,连接、发现工具、调用工具 | 修改 |
| `xiaoqu-ai/claw-code-parity/rust/crates/claw-mobile/src/mcp_http.rs` | Rust HTTP | JSON-RPC HTTP/SSE MCP 客户端 | 修改 |
| `xiaoqu-agent-service/src/main/java/me/iiv/agent/security/AkAuthFilter.java` | 服务鉴权 | 仅解析 `Authorization: Bearer <ak>` | 保持兼容 |
| `xiaoqu-agent-service/src/main/java/me/iiv/agent/security/AuthService.java` | 服务鉴权 | AK SHA-256 哈希查询、用户加载、过期检查 | 修改/复用 |
| `xiaoqu-agent-service/src/main/java/me/iiv/agent/admin/AdminTokenService.java` | 服务 Token | 管理员为指定用户签发 AK,明文只返回一次 | 扩展复用 |
### 2.2 当前缺口
1. `ClawBridge.liveValue` 是静态值,Token 只在进程初始化时读取一次。
2. 当前 FFI 将 `apiKey` 发送为 `X-API-Key`,与服务端要求的 Bearer 不一致。
3. iOS FFI 未把配置文件中的 MCP Server 自动接入 `FfiEngineConfig`。
4. 多 MCP Client 共存时,工具调用没有稳定的“工具名 → 所属 Server”映射。
5. 当前管理端签发接口需要管理员会话,不适合作为普通用户登录后的自动取 Token 接口。
## 3. 方案设计
### 3.1 认证模型
采用两级凭证:
1. xiaoqu-ai 登录 `accessToken`:证明当前用户登录身份,只用于向业务后端换取 MCP Token。
2. MCP Token(`xqak_*`):绑定 `user_id`,受 MCP scope、租户/行级权限、过期时间和撤销状态约束,只用于 `/mcp`。
客户端不得把 `accessToken` 和 MCP Token 混为一谈。MCP 服务仍只接受 MCP Token。
### 3.2 Token 生命周期
| 场景 | 行为 |
|---|---|
| 登录成功 | 用当前用户登录态换取 MCP Token,按用户保存 |
| MCP Token 过期/401 | 清除当前用户 MCP Token,重新换取一次;同一请求不自动重放工具副作用 |
| accessToken 刷新 | 保留 MCP Token;若服务返回 401,再重新换取 |
| 切换账号 | 先断开旧 MCP Client,再加载新账号 Token 并建立新 Client |
| 退出登录 | 销毁旧 Claw/MCP Client,删除当前账号本地 MCP Token;服务端撤销或等待短期过期 |
| 多账号 | Keychain key 必须包含稳定用户 ID,禁止所有账号共用 `mcpToken` |
建议 MCP Token 默认有效期 15 分钟,并提供换取接口;长期 AK 仅用于受控开发/管理场景。
## 4. API 契约
### 4.1 POST `/api/v1/mcp/session-token`
描述: 使用当前 xiaoqu-ai 登录态,为当前用户签发一个短期 MCP Token。
认证:
- `Authorization: Bearer <xiaoqu-access-token>`
- 服务端必须从签名 Token 解析用户和租户,忽略请求体中的身份字段。
请求体:
- `client`: string (required) — 固定为 `xiaoqu-ai-ios`,最长 64 字符。
- `device_id`: string (required) — 设备安装实例 ID,最长 128 字符;不得包含 Token。
响应 200:
- `token`: string (required) — MCP 明文 Token,仅本次返回。
- `expires_at`: datetime (required) — MCP Token 到期时间。
- `user_id`: int (required) — 服务端从登录态解析出的用户 ID。
- `tenant_id`: int (required) — 服务端解析出的租户 ID。
- `scope_version`: string (required) — 本次绑定权限版本,用于客户端诊断,不作为鉴权依据。
错误:
- `401`: 登录态缺失、无效或已过期。
- `403`: 用户无权使用 MCP 或租户未开通。
- `409`: 用户 Token 配置缺失/作用域未配置。
- `429`: 换取频率超限。
- `503`: MCP Token 服务暂不可用;客户端不得携带旧 Token 强行重试写操作。
服务端处理要求:
- 使用现有 `agent_api_key` 哈希存储和 `token_scope` 绑定逻辑。
- 每次签发生成新的随机 Token,明文仅返回一次;不能查询旧 Token 明文。
- 默认只绑定当前用户允许的只读作用域;高风险作用域必须单独审批。
- 签发、刷新、撤销写入审计日志,记录 `user_id`、`tenant_id`、client、device_id、结果和原因,不记录 Token。
### 4.2 POST `/mcp`
描述: MCP Streamable HTTP JSON-RPC 端点,现有接口保持不变。
请求 Header:
- `Authorization: Bearer <mcp-token>` (required)
- `Content-Type: application/json` (required)
- `Accept: application/json, text/event-stream` (required)
- `Mcp-Session-Id: <id>` (required after initialize response contains it)
请求/响应:
- 保持 MCP `initialize` → `notifications/initialized` → `tools/list` → `tools/call` 顺序。
- `tools/call` 必须继续执行 Token 作用域、用户和租户数据范围校验。
错误:
- `401`: MCP Token 缺失、无效、撤销或过期。
- `200` + MCP `isError=true`: Token 有效但无权调用该工具,客户端展示权限错误,不切换账号重试。
### 4.3 客户端配置文件
配置文件只保存非用户敏感配置:
```json
{
"mcpServers": {
"xiaoqu": {
"type": "http",
"url": "http://<host>:8088/mcp"
}
}
}
```
Token 不写入该文件。客户端运行时读取当前用户 Token,并动态加入 `Authorization` Header。
## 5. 客户端实现设计
### 5.1 ClawBridge
- 将 `ClawBridge` 从静态全局凭证改为按当前用户创建的实例。
- `connectMcp(userID, mcpToken)` 创建只属于当前用户的 FFI Engine。
- `disconnectMcp()` 在切换账号、退出登录和重新换 Token 前调用。
- 不在 `liveValue` 捕获 `accessToken` 或 MCP Token;通过依赖注入传入当前账号凭证。
### 5.2 FFI / HTTP
- `FfiMcpServerConfig` 增加明确的认证模式,或直接将 `apiKey` 改为 `bearerToken`。
- 使用 `Authorization: Bearer <mcpToken>`,禁止发送 `X-API-Key`。
- `initialize` 后保存 `Mcp-Session-Id`,发送 `notifications/initialized`,再调用 `tools/list`。
- 工具发现时保存 `tool_name → server_name` 映射;调用只发送到所属 Server,不按 Client 顺序盲试。
- Token 过期只触发一次重新换取和重新连接;并发请求共享刷新锁,避免同一用户并发签发多个 Token。
### 5.3 Keychain
- Key 名:`mcpToken.<user_id>`,值为 MCP Token;使用 ThisDeviceOnly 存储。
- `device_id` 单独生成并持久化,不从 Token 派生。
- 日志只记录 Token 是否存在、剩余有效期和用户 ID,不记录 Token 内容。
## 6. 数据模型变更
### 6.1 现有表复用
| 表 | 用途 | 变更 |
|---|---|---|
| `agent_api_key` | MCP Token 哈希、用户绑定、状态、过期时间 | 复用;确认 `user_id/status/expire_time/key_hash` 索引 |
| `token_scope` | Token 与作用域绑定 | 复用 |
| `audit_log` | MCP 调用和 Token 生命周期审计 | 扩展事件类型/元数据(若当前字段不足) |
### 6.2 建议新增字段(仅在现有字段不足时)
| 表 | 字段 | 类型/约束 | 说明 |
|---|---|---|---|
| `agent_api_key` | `source` | `VARCHAR(32) NOT NULL DEFAULT 'admin'` | `admin` / `user_session`,便于撤销和审计 |
| `agent_api_key` | `device_id` | `VARCHAR(128) NULL` | 用户会话 Token 的设备绑定 |
索引:`(user_id, source, status, expire_time)`。新增字段必须通过新的 Flyway migration,禁止修改已执行 migration。
## 7. 变更文件清单
### xiaoqu-agent-service
| 文件/目录 | 层级 | 类型 | 改动说明 |
|---|---|---|---|
| `security/` JWT 验证组件 | Security | 新增/修改 | 验证 xiaoqu 登录态并解析 user/tenant,拒绝客户端自报身份 |
| `mcp/` Token exchange service | Service | 新增 | 签发短期用户 MCP Token、作用域绑定、限流和审计 |
| `handler/` MCP credential controller | Handler | 新增 | 实现 `/api/v1/mcp/session-token` |
| `security/AkAuthFilter.java` | Filter | 修改 | 保持 Bearer MCP AK 兼容,补齐撤销/错误审计要求 |
| `db/migration/V__user_session_token.sql` | Migration | 条件新增 | 只有字段/索引确有需要时新增 |
| `src/test/...` | Test | 新增 | 用户隔离、过期、撤销、越权、限流和不记录明文测试 |
### xiaoqu-ai
| 文件/目录 | 层级 | 类型 | 改动说明 |
|---|---|---|---|
| `ios/.../ClawBridge.swift` | iOS | 修改 | 按登录用户注入 Token,重建/销毁 Claw 实例 |
| `claw-mobile/src/ffi.rs` | Rust FFI | 修改 | Bearer Header、会话通知、Server 路由映射 |
| `claw-mobile/src/mcp_http.rs` | Rust HTTP | 修改 | 通用 Header、Session ID 和 JSON-RPC 初始化流程 |
| `ios/.../KeychainClient.swift` | iOS 存储 | 修改 | 账号隔离的 MCP Token 存取和删除 |
| `ios/.../AuthFeature.swift` | 登录生命周期 | 修改 | 登录/刷新/切换/退出触发 MCP 凭证生命周期 |
| `ios/Tests/...` | Test | 新增 | 用户切换、401 刷新、Token 不串号测试 |
| `claw-code-parity/.../settings*.json` | 配置 | 修改 | 仅保存 URL,不保存用户 Token |
## 8. 开发任务拆分
| # | 任务标题 | 工时 | 依赖 | 完成标准 |
|---|---|---:|---|---|
| 1 | 服务端确认 JWT 与租户映射、Token exchange 契约 | 2h | - | 401/403/409/429 语义和 scope 默认值确定 |
| 2 | 服务端实现用户 MCP Token 签发与审计 | 4h | #1 | Token 只返回一次,哈希落库,绑定 user/tenant/scope |
| 3 | iOS Keychain 账号隔离和认证生命周期接入 | 3h | #1 | 登录/切换/退出无旧 Token 残留 |
| 4 | Rust MCP HTTP Bearer、Session、初始化通知 | 3h | #3 | initialize/list/call 真实握手成功 |
| 5 | MCP 工具到 Server 路由映射 | 2h | #4 | 多 Server 下工具调用不发错目标 |
| 6 | 集成与安全回归测试 | 2-4h | #2-#5 | 用户隔离、401 刷新、权限拒绝和日志脱敏通过 |
实施顺序:#1 → #2;#3 与 #4 可并行;完成后 #5 → #6。
## 9. 测试与验收
### 9.1 服务端
- 用户 A Token 调用 `tools/list` / `tools/call` 成功,审计 `user_id=A`。
- 用户 B Token 调用时只能看到/调用 B 作用域允许的数据。
- A 的 Token 调用不存在或已撤销 Token 返回 401;A 的 Token 调 B 身份参数不得改变审计身份。
- A 无某工具 scope 时返回 MCP `isError=true`,不得执行 Handler。
- 签发响应后数据库和日志中搜索不到 Token 明文。
### 9.2 iOS
- A 登录后抓包确认所有 MCP 请求为 `Authorization: Bearer <A-MCP-Token>`。
- 切换 B 后,旧连接关闭,后续请求只携带 B Token。
- A Token 401 时只刷新 A 的 Token,不影响 B 或其他账号。
- 退出后旧 Claw 不再产生网络请求。
- 配置文件、prompt、工具参数和日志不包含 Token。
### 9.3 联调命令
```bash
# 使用当前用户登录态换 MCP Token(示例,不把 Token 写入仓库)
curl -X POST http://<host>/api/v1/mcp/session-token \
-H 'Authorization: Bearer <xiaoqu-access-token>' \
-H 'Content-Type: application/json' \
-d '{"client":"xiaoqu-ai-ios","device_id":"<device-id>"}'
# 使用返回的用户 MCP Token 完成 MCP 握手
curl -X POST http://<host>:8088/mcp \
-H 'Authorization: Bearer <user-mcp-token>' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"xiaoqu-ai","version":"1.0"}}}'
```
## 10. 技术风险与回退
| 风险 | 等级 | 应对 |
|---|---|---|
| accessToken 与 MCP AK 类型混用 | 高 | 强制 Token exchange;服务端只接受明确的 MCP AK |
| 静态 Claw 实例持有旧用户 Token | 高 | 用户切换/退出先销毁实例,增加生命周期测试 |
| 多 MCP Server 工具路由错误 | 高 | 建立工具归属映射,不采用首个 Client 试调用 |
| Token 泄露到日志/崩溃 | 高 | 脱敏中间件、禁止打印 Header、静态扫描和测试 |
| Token exchange 不可用 | 中 | MCP 功能降级为不可用并提示重试;不得使用公共 Token 兜底 |
| 新字段迁移失败 | 中 | 字段变更独立 Flyway migration;回滚只撤销新增字段/索引 |
回退策略:保留现有管理员签发 Token 和 `/mcp` 协议不变;先关闭用户自动换取入口,客户端显示 MCP 不可用。禁止回退到全局共享 Token。
## 11. 设计结论
公共 MCP 配置文件只负责“连哪个服务”;用户 Token 由登录用户上下文动态决定。实现完成后,xiaoqu-ai 的每个用户拥有独立 MCP 权限边界,服务端仍能按用户、租户、作用域和审计链路追踪调用。