6.7 KiB
6.7 KiB
version, last-updated, status
| version | last-updated | status |
|---|---|---|
| 1.1 | 2026-03-19 | stable |
认证机制与 SavedKey 生命周期
本文档说明 Qiming Agent 客户端的认证设计,重点描述
savedKey的作用、存储结构、完整生命周期,以及退出登录与服务启动的联动逻辑。
核心概念
两类认证凭证
| 凭证 | 存储键 | 说明 | 退出登录后 |
|---|---|---|---|
configKey |
auth.config_key |
当前会话的登录态 token,从服务端 /register 接口获取 |
清除 |
savedKey |
auth.saved_key |
设备级注册凭证("记住我" token),与 configKey 值相同,但跨登录会话持久化 | 保留 |
重要:
savedKey与configKey值相同(均为服务端返回的response.configKey),区别在于持久化策略不同。savedKey的存在代表"这台设备已完成注册,可以免密重新认证"。
多账号隔离存储
savedKey 按 domain + username 维度分别存储,支持同一设备多账号切换:
auth.saved_key ← 全局快速访问(最后一次登录的账号)
auth.saved_keys.<domain>_<username> ← 域名+用户级持久化(每个账号独立)
键名与迁移:存储键中的 <domain> 由 normalizeDomain()(auth.ts)生成(hostname 或规范化字符串)。若未来调整域名规范化规则(如大小写、去端口、trailing slash),需考虑旧键兼容或提供迁移逻辑,避免已存账号无法匹配。
完整生命周期
第一次登录(全新设备/账号)
用户输入 domain + username + password
│
└── getSavedKey(domain, username)
└── 未找到 → savedKey = undefined
│
└── registerClient({ username, password, savedKey: undefined })
│
└── 服务端返回 { configKey, ... }
│
├── setConfigKey(configKey)
│ └── auth.config_key = configKey
│
└── setSavedKey(configKey, domain, username)
├── auth.saved_keys.<domain>_<username> = configKey
└── auth.saved_key = configKey
退出登录
用户点击"退出登录"(ClientPage.tsx → handleLogout)
│
├── 1. 停止所有运行中的服务(按顺序)
│ ├── agent.destroy()
│ ├── fileServer.stop()
│ ├── lanproxy.stop()
│ ├── mcp.stop()
│ └── computerServer.stop() ← 登出时单独停止,避免进程残留与端口占用
│
└── 2. logout() → clearAuthInfo()
├── auth.username = null
├── auth.config_key = null ← 登录态清除
├── auth.user_info = null
├── auth.online_status = null
└── auth.saved_key = 保留 ← 设备凭证不清除,下次可免密重连
注意:密码从不持久化保存。登录时用户输入的密码仅用于本次认证请求,不存入数据库。
重新打开 App(有 savedKey,自动重连)
App 启动 → autoReconnect(App.tsx)
│
├── 检查 loginStartedRef.current(内存变量)
│ └── 已由登录流程启动 → 跳过(同一会话内有效)
│
├── 检查 setupJustCompleted
│ └── 向导刚完成 → 直接启动服务(不走重连)
│
└── 读取 auth.saved_key
├── 不存在 → 跳过(首次使用,需手动登录)
│
└── 存在 → syncConfigToServer({ suppressToast: true })
│
├── 成功(服务端接受 savedKey)
│ ├── 更新 auth.config_key(新 token)
│ ├── 更新 auth.saved_key(续期)
│ ├── 更新 auth.online_status / user_info
│ └── startServicesSequentially(
│ ['mcpProxy', 'agent', 'fileServer', 'lanproxy']
│ )
│
└── 失败(网络断开 / token 过期 / 账号被封)
└── 服务不启动,停留在登录页
用户再次登录(已有 savedKey 的账号)
用户输入 domain + username + password
│
└── getSavedKey(domain, username)
└── 找到 → savedKey = "<持久化的 configKey>"
│
└── registerClient({ username, password, savedKey })
└── 服务端识别为老设备,直接续期返回新 configKey
切换账号(不同 domain 或 username)
用户输入 domainB + userB(与上次不同)
│
└── getSavedKey(domainB, userB)
└── 未找到 → savedKey = undefined
│
└── registerClient({ username, password, savedKey: undefined })
└── 服务端视为全新设备注册
服务与登录状态的联动规则
| 场景 | 服务行为 |
|---|---|
| 当前会话退出登录 | 立即停止所有运行中的服务 |
| 重启 App,有 savedKey,重连成功 | 自动启动全部服务(mcpProxy → agent → fileServer → lanproxy) |
| 重启 App,有 savedKey,重连失败 | 服务不启动,停留在登录页 |
| 重启 App,无 savedKey | 服务不启动,停留在登录页 |
| 手动登录成功 | 由 onComplete 触发服务启动(不走 autoReconnect) |
服务启动顺序固定:
mcpProxy必须先于agent,因为 Agent 初始化时需要连接 MCP Proxy 注入 mcpServers。
相关代码位置
| 逻辑 | 文件 | 关键函数/位置 |
|---|---|---|
| savedKey 存取 | src/renderer/services/core/auth.ts |
getSavedKey() / setSavedKey() L86-100 |
| 退出登录清理 | src/renderer/services/core/auth.ts |
clearAuthInfo() L131-138 |
| 停止服务+登出 | src/renderer/components/pages/ClientPage.tsx |
handleLogout() L171-200 |
| 自动重连逻辑 | src/renderer/App.tsx |
autoReconnect effect L297-349 |
| 静默重新认证 | src/renderer/services/core/auth.ts |
syncConfigToServer() L383-458 |
| 注册/登录 | src/renderer/services/core/auth.ts |
loginAndRegister() L209-314 |