Files
xiaoqu_mcp_service/xiaoqu-agent-service/doc/xiaoqu-ai-mcp-per-user-token-design.md
T

18 KiB
Raw Blame History

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 接口。 已解决(2026-09-04):xiaoqu-agent-service 已交付 Open API POST /api/open/token/ensure(get-or-create,服务间密钥鉴权),见 §4.1。

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;当前永久 Token 不会自动失效,需要显式调用 revoke
多账号 Keychain key 必须包含稳定用户 ID,禁止所有账号共用 mcpToken

建议 MCP Token 默认有效期 15 分钟,并提供换取接口;长期 AK 仅用于受控开发/管理场景。

2026-09-04 更新(与已交付实现对齐):现行模型为长期有效 + 禁用回收(与管理后台签发模型一致),不做 15 分钟短周期。ensure 语义天然兼容短周期模式(过期 token 不计入"已存在",会触发新建并返回新明文),将来如需缩短有效期,只调整签发时的 expireTime,协议不变。

4. API 契约

2026-09-04 更新:本节按 xiaoqu-agent-service 已交付的 Open API 重写。核心变化——agent-service 不直接验证 xiaoqu-ai 登录态(它没有 JWT 基础设施,也不懂租户),改为两段式:iOS 用登录态调 xiaoqu-ai 业务后端;业务后端拿着登录用户的手机号/邮箱,用服务间密钥调 agent-service 的 ensure 接口取 MCP Token。

4.1 服务端原语(已交付):POST /api/open/token/ensure

描述: 按 手机号/邮箱 + 作用域编码 确保用户有有效 MCP Token(get-or-create)。

认证:

  • X-Open-Key: <服务间静态密钥>(dev 配置在 application-dev.yml 的 app.open.api-key,生产走环境变量;配置为空 = 接口关闭)。
  • 只允许 xiaoqu-ai 业务后端等服务端调用方持有,不得下发到 App。

请求体:

  • mobile: string — 登录用户手机号;与 email 至少传一个,手机号优先。
  • email: string — 登录用户邮箱。
  • scopeCode: string (required) — 作用域编码(如 operation);iOS 用哪个作用域由双方约定后在此固定。

响应 200:

  • created: bool — 本次是否新建。
  • token: string | null — 仅 created=true 时返回明文,且只出现这一次;created=false 时恒为 null。
  • keyPrefix: string — Token 前缀(如 xqak_abcd1234),用于标识和对账。
  • expireTime: datetime | null — 现行为 null(永久,靠禁用回收)。
  • userId / userName / scopeCode: 归属信息。

错误(统一 {"message": "原因"}):

  • 400: 参数缺失 / 作用域不存在或已停用。
  • 401: X-Open-Key 无效。
  • 403: Open API 未启用(服务端未配置密钥)。
  • 404: 人员不存在或非企业账号(只认 enterprise_type 0/1 的企业账号)。

服务端处理(已实现):

  • 复用 agent_api_key 哈希存储与 token_scope 绑定;一作用域一 Token。
  • 已存在有效 Token(正常且未过期)只回元信息——明文不可回吐,服务端只存 SHA-256。
  • 不存在则自动创建永久 Token,memo 标记 open-api 自动签发(<scopeCode>)。
  • 全程写 audit_log(tool_name=open.token.ensure、client_type=open-api,手机号/邮箱脱敏,不记录 Token)。

关键约束与对策:created=false 时拿不到明文。因此 xiaoqu-ai 业务后端必须在首次 created=true 时自己持久化明文(按 user_id 关联,加密存储),之后下发给本用户的客户端。

身份约束:业务后端不得接受客户端或模型传入的 user_id、mobile、email 作为身份依据。必须先从已验证的 xiaoqu-ai accessToken 会话解析用户,再由业务后端查询该用户的手机号/邮箱并调用 ensure。X-Open-Key 只允许服务端保管。

4.1a Token 丢失与主动失效(建议补充)

当前 ensure 在 created=false 时不会返回明文,无法解决业务后端首次落库失败或 Token 泄露后的换发问题。建议增加两个服务间接口,均使用 X-Open-Key 鉴权:

POST /api/open/token/rotate

  • 按用户和 scopeCode 禁用当前有效 Token,创建新 Token。
  • 仅本次返回新 Token 明文;旧 Token 立即失效。
  • 记录旧 Token、新 Token 前缀、用户和 scope,禁止记录 Token 明文。
  • 具备幂等保护,避免并发换发产生多个有效 Token。

POST /api/open/token/revoke

  • 按用户和 scopeCode 禁用当前 Token。
  • 不返回 Token 明文;重复调用视为成功。
  • 用于退出登录后的主动失效、设备解绑和安全事件处置。

在 rotate/revoke 尚未实现前,退出登录只能清理客户端本地凭证,不能声称服务端 Token 已失效;管理员后台禁用仍是唯一服务端回收方式。

4.1b xiaoqu-ai 业务后端门面(待 xiaoqu-ai 侧实现)

iOS 面向的接口(建议沿用原设计):POST /api/v1/mcp/session-token,Authorization: Bearer <xiaoqu-access-token>。业务后端从登录态解析用户 → 服务端查询手机号/邮箱 → 调 ensure → 返回 {token, expires_at, user_id}。不得使用请求体中的 user_id/mobile/email 覆盖登录态身份;客户端身份字段(client、device_id)由业务后端自行记录,agent-service 不感知。

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 客户端配置文件

配置文件只保存非用户敏感配置:

{
  "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 建议新增字段(仅在现有字段不足时)

2026-09-04 注:已交付实现暂不新增字段——ensure 自动签发的 Token 用 memo 标记(open-api 自动签发(<scopeCode>)),create_id=0 表示系统签发。若后续需要按来源/设备撤销或统计,再按下表加独立 migration。

表 字段 类型/约束 说明
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 新增 取消 agent-service 不验证 xiaoqu-ai 登录态,改由业务后端两段式调用
open/OpenTokenController.java 等 Open API 已交付(2026-09-04) POST /api/open/token/ensure,X-Open-Key 鉴权,get-or-create
open/OpenKeyFilter.java Filter 已交付 服务间静态密钥,常量时间比较,配置为空即关闭
admin/AdminTokenService.java Service 已交付 一作用域一 Token;issueSingle/findActiveToken 供 ensure 复用
common/GlobalExceptionHandler.java Handler 已交付 错误统一 {"message": ...}
security/AkAuthFilter.java Filter 保持兼容 /mcp Bearer ak 校验不变
src/test/...open Test 已交付 ensure 已存在/新建/404/停用作用域/缺参 5 个用例

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 契约 → xiaoqu-ai 业务后端实现门面接口(登录态 → ensure) 2h - 门面 401/403 语义确定;明文按 user_id 加密落库
2 服务端实现用户 MCP Token 签发与审计 已交付(ensure + 审计,2026-09-04) - - 已上线并实测幂等/脱敏/错误语义
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 联调命令

# 业务后端侧:用服务间密钥 ensure 用户 MCP Token(示例,密钥与明文不写入仓库)
curl -X POST http://<host>:8088/api/open/token/ensure \
  -H 'X-Open-Key: <服务间密钥>' \
  -H 'Content-Type: application/json' \
  -d '{"mobile":"<登录用户手机号>","scopeCode":"operation"}'

# iOS 侧:走 xiaoqu-ai 业务后门门面(待实现)
curl -X POST http://<xiaoqu-ai-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 权限边界,服务端仍能按用户、租户、作用域和审计链路追踪调用。