From df8a995ec4366382bf8c0c6227fdd1327bef874a Mon Sep 17 00:00:00 2001 From: TraeAI Date: Sat, 20 Jun 2026 23:42:34 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E9=9B=86=E4=B8=AD=E9=89=B4=E6=9D=83?= =?UTF-8?q?=E4=B8=8E=20Token=20=E6=8C=81=E4=B9=85=E5=8C=96=E9=9C=80?= =?UTF-8?q?=E6=B1=82=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 覆盖 A+B+C 一次到位方案: - A 当前实现加固(refresh 轮换、记住我、预刷新、启动三态) - B Tauri OS Keychain 集成(keyring crate 跨 macOS/Win/Linux) - C 服务端 Session 表(滑动过期、踢出、改密码强踢、reuse 检测) Out of scope: 企业 IdP / SSO / 2FA / 多租户(后续单独 brainstorm) --- ...zed-auth-token-persistence-requirements.md | 522 ++++++++++++++++++ 1 file changed, 522 insertions(+) create mode 100644 docs/brainstorms/2026-06-20-centralized-auth-token-persistence-requirements.md diff --git a/docs/brainstorms/2026-06-20-centralized-auth-token-persistence-requirements.md b/docs/brainstorms/2026-06-20-centralized-auth-token-persistence-requirements.md new file mode 100644 index 0000000..dc886da --- /dev/null +++ b/docs/brainstorms/2026-06-20-centralized-auth-token-persistence-requirements.md @@ -0,0 +1,522 @@ +# Fischer AgentKit — 集中鉴权与 Token 持久化 (Requirements) + +**Date:** 2026-06-20 +**Branch:** `feat/centralized-auth-token-persistence` +**Status:** Draft +**Scope:** 服务端签发 JWT + 客户端安全持久化 + 服务端 Session 表 + Refresh Token 轮换 + 「记住我」 + 启动态区分 +**Out of scope:** 企业 IdP 对接(OIDC / SAML / LDAP)、多租户、密码强度策略、2FA、SSO 跳转 + +--- + +## 1. Context & Problem + +### 1.1 现状 + +**已经实现(但不够健壮)**: + +- [auth/models.py](file:///Users/Chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/server/auth/models.py) SQLite 存用户 + bcrypt 哈希(cost=12)+ session hash +- [auth/password.py](file:///Users/chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/server/auth/password.py) bcrypt 哈希 / 验证 +- [routes/auth.py](file:///Users/chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/server/routes/auth.py) `/api/v1/auth/login` `/refresh` `/logout` 接口 +- [stores/auth.ts](file:///Users/chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/server/frontend/src/stores/auth.ts) Pinia 鉴权 store,access (15min) + refresh (7d) + user 存 localStorage +- [router/index.ts](file:///Users/chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/server/frontend/src/router/index.ts) 路由守卫读 store,已有 token 时自动放行 +- [views/LoginView.vue](file:///Users/chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/server/frontend/src/views/LoginView.vue) `onMounted` 检查 `isAuthenticated` 自动跳走 +- [api/base.ts](file:///Users/chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/server/frontend/src/api/base.ts) 401 → `refreshIfPossible` → 失败清空 + +**暴露的问题**: + +1. **Token 持久化安全弱** — access + refresh + user **明文**存 WebView localStorage(macOS: `~/Library/WebKit/.../LocalStorage/`),被其他进程或恶意浏览器扩展可读 +2. **无 refresh token 轮换** — 同一 refresh token 可在 7 天内无限复用,被泄漏后无重置窗口 +3. **无服务端 Session 表** — 改密码 / 踢人 / 看活跃设备 全部做不到;`user_sessions` 表([auth/models.py:55-66](file:///Users/chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/server/auth/models.py#L55-L66))只存 refresh token 哈希,看不到设备/时间/IP +4. **无「记住我」开关** — refresh 固定 7 天,没法选「这台设备记 30 天」 +5. **无预刷新** — access 还有效时不主动续,依赖 401 触发;批量请求高峰可能集中刷新 +6. **启动态不区分** — 有 token 但服务端 401 时直接清掉,看不出「我刚登录过 / 现在连不上」的区别 +7. **集团统一管理缺位** — admin 没法在「企业员工离职 / 设备丢失 / 异常登录」场景下强制踢出 + +### 1.2 目标 + +- 首次登录后,**冷启动应用直接进主界面**,不显示登录页(除非主动登出 / token 失效) +- 鉴权逻辑 **100% 在服务端完成**(已经是这样,但需要加强) +- Token 走 **OS Keychain 加密存储**(Tauri 桌面),Web 端 fallback 到 localStorage +- 服务端 **Session 表**支持:滑动过期 / 主动踢出 / 改密码失效 / 活跃设备列表 +- Refresh token **每次刷新轮换**,旧 token 立即失效 +- 支持 **「记住我」**(refresh 7d / 30d) +- 客户端 access token **预刷新**(剩余 <2min 主动 refresh) +- 启动时区分 `no_token` / `token_invalid` / `token_valid` 三态 + +### 1.3 设计哲学 + +> **服务端权威 + 客户端最小信任 + 加密落盘 + 可观测可治理** + +- **服务端权威**:所有 token 校验、续期、撤销由服务端说了算,客户端不可绕过 +- **客户端最小信任**:客户端不存密码、access token 不持久化(仅内存)、refresh token 进 Keychain +- **加密落盘**:refresh token 走 OS 级加密(Tauri: macOS Keychain / Windows Credential Manager / Linux Secret Service) +- **可观测可治理**:admin 能看 / 踢任意 session,集团统一管理的基础设施准备好 + +--- + +## 2. Goals + +### 2.1 Functional Goals + +| ID | 目标 | 用户感知 | +|----|------|----------| +| F1 | 首次登录后冷启动直接进应用 | 第二次开 App 看不到登录页 | +| F2 | 「记住我」延长 refresh 到 30 天 | 登录页 checkbox,不勾选时 refresh 7 天 | +| F3 | Tauri 端 refresh token 存 OS Keychain | 进程外 / 其他用户 / 恶意扩展都拿不到 | +| F4 | Web 端 refresh token 仍可存 localStorage(fallback) | 浏览器用户也能用,只是降级安全 | +| F5 | Refresh token 每次使用后轮换 | 旧 token 调 `/refresh` 立即被拒 | +| F6 | 服务端 Session 表记录每次登录的设备/IP/时间 | 登录日志可审计 | +| F7 | admin 可看任意用户的活跃 session 列表 | 「谁在哪里登录」可见 | +| F8 | admin / 用户本人可踢出指定 session | 设备丢失立即失效 | +| F9 | 用户改密码立即使所有 session 失效 | 全设备强踢 | +| F10 | 客户端 access token 剩余 <2min 主动 refresh | 不依赖 401 触发 | +| F11 | 启动区分 `no_token` / `token_invalid` / `token_valid` | 错误态有「重试」按钮而不是直接清空 | +| F12 | 多个 Tauri / Web 客户端可同时登录同一账号 | 互不干扰,独立 session | + +### 2.2 Non-Functional Goals + +| ID | 目标 | 度量 | +|----|------|------| +| N1 | Token 校验 P99 < 5ms | 加上 session 表后用 Redis cache | +| N2 | Keychain 读写失败 fallback 到 localStorage | 不阻塞登录 | +| N3 | Session 表 + 索引不阻塞 1k RPS 登录 | 索引 `(user_id, revoked, expires_at)` | +| N4 | 旧 refresh token 泄漏检测 | 每次 refresh 检测「被轮换过的 token 再用」→ 全 session 失效 | +| N5 | 所有鉴权代码通过单测 + 集成测 | `pytest tests/unit/auth/` + `tests/integration/auth/` | +| N6 | 兼容旧版本客户端的 JWT(7d 滚动)至少 1 个 minor 版本 | 灰度期间双轨 | + +--- + +## 3. Non-Goals + +- ❌ **企业 IdP 对接**(OIDC / SAML / LDAP / 飞书 / 钉钉 / 企微)— 下一迭代单独 brainstorm +- ❌ **多租户 / 集团多组织隔离** — 当前单租户架构不动 +- ❌ **密码强度策略 / 密码过期 / 密码历史** — 单独的 IAM 改造 +- ❌ **2FA / TOTP / WebAuthn / Passkey** — 单独 brainstorm +- ❌ **前端账号注册流程** — 当前只支持 admin 创建用户 +- ❌ **登录失败锁定 / 滑窗限流** — 单独的安全加固 +- ❌ **审计日志的全文检索 / 导出** — 只记录关键事件到 session 表,复杂审计后续做 + +--- + +## 4. User Scenarios + +### 4.1 普通员工「每天开应用」 + +1. 早上打开 Tauri 客户端 +2. 启动页 → 后端健康检查 → `/api/v1/auth/whoami` 携带 Keychain 里的 refresh token 调一次 +3. 服务端校验通过 → 返回新 access token + user → 进主界面 +4. **看不到登录页** + +### 4.2 设备丢失「管理员踢人」 + +1. 员工报设备丢失 +2. 管理员在「用户管理 → 会话列表」看到该员工所有活跃 session +3. 点击「踢出」 → 服务端 `UPDATE auth_sessions SET revoked=1 WHERE id=?` +4. 该设备下一次请求 401 → 客户端清空 → 跳登录页 + +### 4.3 改密码「全设备强踢」 + +1. 用户在「设置 → 修改密码」提交新密码 +2. 服务端更新密码哈希 + `UPDATE auth_sessions SET revoked=1 WHERE user_id=? AND id != ?`(保留当前 session) +3. 其他设备下次请求 401 → 自动跳登录 + +### 4.4 「记住我」开关 + +1. 登录页有「记住我(30 天)」checkbox +2. 不勾选 → refresh 7 天;勾选 → refresh 30 天 +3. 用户可在「设置 → 安全」随时查看/撤销当前 session + +### 4.5 Refresh Token 泄漏检测 + +1. 攻击者拿到旧的 refresh token +2. 调 `/api/v1/auth/refresh` → 服务端检测「此 token 已被轮换过」 +3. **整个 user 的所有 session 立即撤销**(防扩散) +4. 推送通知到用户「检测到异常登录」 + +--- + +## 5. Functional Requirements + +### 5.1 服务端 — Session 表 + API + +#### 5.1.1 新增表 `auth_sessions` + +| 字段 | 类型 | 索引 | 说明 | +|------|------|------|------| +| `id` | TEXT PK (uuid) | — | session 标识,JWT sub claim 引用 | +| `user_id` | INTEGER FK→users | `(user_id, revoked, expires_at)` | 拥有者 | +| `device_fingerprint` | TEXT | — | User-Agent + platform hash | +| `device_label` | TEXT | — | 「macOS Chrome 119」/「Windows Tauri 1.0」 | +| `ip` | TEXT | — | 登录 IP | +| `user_agent` | TEXT | — | 原始 UA | +| `created_at` | INTEGER | — | 登录时间 | +| `last_active_at` | INTEGER | — | 最近一次 refresh 验证时间 | +| `expires_at` | INTEGER | `(expires_at)` | refresh 过期时间(绝对) | +| `revoked` | INTEGER (bool) | — | 是否被踢 | +| `revoked_reason` | TEXT NULL | — | `user_terminated` / `password_changed` / `admin_revoked` / `reuse_detected` | +| `previous_session_id` | TEXT NULL | — | refresh 轮换的上一跳(用于审计) | + +#### 5.1.2 JWT payload 扩展 + +```json +{ + "sub": "user_id", + "sid": "session_id", // 新增:关联 auth_sessions.id + "type": "access", + "exp": ..., + "iat": ..., + "jti": "..." // 新增:单次 token id,用于黑名单 +} +``` + +Refresh token 同样携带 `sid`,校验时必须查 `auth_sessions` 表确认未 revoked。 + +#### 5.1.3 新增 / 修改 API + +| Method | Path | 鉴权 | 行为 | +|--------|------|------|------| +| POST | `/api/v1/auth/login` | 无 | 校验密码 → 创建 session → 返回 access+refresh+JWT(sid) | +| POST | `/api/v1/auth/refresh` | refresh | 校验 session 未 revoked → 轮换 refresh → 返回新 pair,旧 refresh 入黑名单 30s | +| POST | `/api/v1/auth/logout` | access | revoke 当前 session | +| GET | `/api/v1/auth/whoami` | access | 返回 user + 当前 session 元数据(设备/IP/时间) | +| GET | `/api/v1/auth/sessions` | access | 当前用户的活跃 session 列表 | +| DELETE | `/api/v1/auth/sessions/{id}` | access | 踢出指定 session(自己踢自己;admin 踢任意) | +| POST | `/api/v1/auth/logout-others` | access | 踢出除当前外的所有 session | +| GET | `/api/v1/admin/users/{id}/sessions` | admin | admin 看任意用户的 session | +| DELETE | `/api/v1/admin/users/{id}/sessions/{sid}` | admin | admin 踢任意 session | +| POST | `/api/v1/auth/change-password` | access | 改密码 + revoke 其他 session | + +#### 5.1.4 Refresh 轮换检测逻辑 + +```python +async def rotate_refresh(old_refresh: str) -> TokenPair: + payload = jwt.decode(old_refresh) + session = await db.get_session(payload["sid"]) + if not session or session.revoked: + raise TokenReuseDetected() # 触发全用户 session 撤销 + if session.refresh_token_hash != sha256(old_refresh): + raise TokenReuseDetected() + # 生成新的 refresh + access,旧 refresh 入「recently_revoked」set(30s) + new_refresh = sign_jwt({...payload, "jti": uuid4()}) + session.refresh_token_hash = sha256(new_refresh) + session.last_active_at = now() + session.expires_at = now() + remember_me_ttl + await db.commit() + return TokenPair(access=..., refresh=new_refresh) +``` + +### 5.2 客户端 — Tauri Keychain 集成 + +#### 5.2.1 新增 Tauri 命令(Rust) + +```rust +// src-tauri/src/auth.rs +#[tauri::command] +async fn store_refresh_token(token: String) -> Result<(), String> { + let entry = keyring::Entry::new("agentkit", "refresh_token")?; + entry.set_password(&token)?; + Ok(()) +} + +#[tauri::command] +async fn load_refresh_token() -> Result, String> { + let entry = keyring::Entry::new("agentkit", "refresh_token")?; + match entry.get_password() { + Ok(t) => Ok(Some(t)), + Err(keyring::Error::NoEntry) => Ok(None), + Err(e) => Err(e.to_string()), + } +} + +#[tauri::command] +async fn clear_refresh_token() -> Result<(), String> { + let entry = keyring::Entry::new("agentkit", "refresh_token")?; + let _ = entry.delete_credential(); + Ok(()) +} +``` + +**依赖**:`keyring = "3"`(macOS Keychain / Windows Credential Manager / Linux Secret Service 统一 API) + +#### 5.2.2 前端 Tauri API 封装 + +```typescript +// src/api/tauri-auth.ts +const isTauri = (): boolean => '__TAURI__' in window + +export const tauriAuthStorage = { + async setRefreshToken(token: string): Promise { + if (isTauri()) { + try { + await invoke('store_refresh_token', { token }) + return + } catch (e) { + console.warn('Keychain 写入失败,fallback 到 localStorage', e) + } + } + localStorage.setItem('agentkit.refresh_token', token) + }, + async getRefreshToken(): Promise { + if (isTauri()) { + try { + return await invoke('load_refresh_token') + } catch (e) { + console.warn('Keychain 读取失败,fallback', e) + } + } + return localStorage.getItem('agentkit.refresh_token') + }, + async clearRefreshToken(): Promise { + if (isTauri()) { + try { await invoke('clear_refresh_token') } catch {} + } + localStorage.removeItem('agentkit.refresh_token') + }, +} +``` + +#### 5.2.3 auth store 改造 + +- 初始化时**异步**从 Keychain 读 refresh token,**不再从 localStorage 同步读** +- 启动时调 `whoami`,分三态: + - `valid` → 直接进应用 + - `invalid` → 显示「会话已过期,请重新登录」+「重试」按钮 + - `error`(网络) → 显示「无法连接服务器」+「重试」按钮 +- access token **只存内存**(不写 localStorage / Keychain) +- user 缓存到 localStorage(明文可接受,仅用于启动时先显示头像 / 角色) + +#### 5.2.4 预刷新逻辑 + +```typescript +// src/api/base.ts 拦截器 +apiClient.interceptors.request.use(async (config) => { + const auth = useAuthStore() + if (auth.accessToken && auth.shouldRefresh()) { // < 2min + await auth.silentRefresh() + } + if (auth.accessToken) { + config.headers.Authorization = `Bearer ${auth.accessToken}` + } + return config +}) +``` + +#### 5.2.5 「记住我」 UI + +- LoginView 加 checkbox「记住我(30 天)」 +- 登录请求带 `?remember_me=true` query(或 body 字段) +- 服务端根据这个决定 refresh TTL + +### 5.3 数据库迁移 + +- 新增 Alembic migration:`add_auth_sessions_table` +- 旧 `user_sessions` 表(refresh token hash)废弃,保留 1 个 minor 版本做兼容 +- Migration 自动回填:现存有效 refresh token → 创建对应 session 行 + +### 5.4 灰度兼容 + +- 新 JWT 携带 `sid` claim +- 旧 JWT 没有 `sid` → 服务端 fallback 到 `user_sessions` 表校验 +- 客户端版本检查:`Authorization` header 带 `X-Client-Version` header +- 服务端支持 1 个 minor 版本的旧客户端(~30 天灰度) + +--- + +## 6. Non-Functional Requirements + +### 6.1 安全 + +| 项 | 要求 | +|---|------| +| Token 存储 | Tauri: OS Keychain(明文只存内存中临时 access) | +| Token 传输 | 强制 HTTPS(生产);dev 允许 HTTP localhost | +| Token 签名 | HS256,密钥 256 bit,存 `.env` `AGENTKIT_JWT_SECRET` | +| Token 算法 | access 15min,refresh 7d / 30d(remember_me) | +| Refresh 轮换 | 每次 refresh 必轮换,旧 token 入短窗口黑名单(30s) | +| 密码哈希 | bcrypt cost=12(已实现) | +| 常量时间比较 | `hmac.compare_digest`(已实现) | +| 审计日志 | session 表 + `last_active_at` + `revoked_reason` | +| 异常检测 | refresh token reuse → 全用户 session 撤销 + 写审计 | + +### 6.2 性能 + +| 项 | 指标 | +|---|------| +| Token 校验 P99 | < 5ms(Redis cache session 元数据) | +| Session 列表查询 P99 | < 50ms(user_id 索引) | +| Redis cache TTL | 60s(session 元数据) | +| Keychain 读写 | < 10ms(macOS Keychain 实际 1-3ms) | + +### 6.3 兼容 + +| 项 | 要求 | +|---|------| +| 旧客户端 | 至少 1 个 minor 版本仍可登录(带 sid 字段的 JWT 也认) | +| Web 端 | localStorage fallback,Keychain 不可用时降级 | +| Tauri 平台 | macOS / Windows / Linux 三端 keychain 都支持(keyring crate) | + +### 6.4 可观测 + +| 项 | 做法 | +|---|------| +| 登录成功 | logger.info + 计数 `auth.login.success` | +| 登录失败 | logger.warn + 计数 `auth.login.failed` | +| Refresh 成功 | logger.debug + 计数 `auth.refresh.success` | +| Refresh reuse 检测 | logger.error + 计数 `auth.refresh.reuse_detected` + 推 audit event | +| Session 撤销 | logger.info + 写入 `revoked_reason` | + +--- + +## 7. Architecture Changes + +### 7.1 改动文件清单(实现时确认) + +**Backend(新增)**: +- `src/agentkit/server/auth/session.py` — session CRUD + 轮换逻辑 +- `src/agentkit/server/auth/jwt_utils.py` — 扩展 JWT payload(加 sid / jti) +- `src/agentkit/server/auth/keychain_audit.py` — refresh reuse 检测 +- `src/agentkit/server/routes/auth.py` — 修改 login/refresh/logout,新增 whoami/sessions/change-password +- `src/agentkit/server/routes/admin.py`(或 auth 内部)— admin session 管理 +- `migrations/versions/xxx_add_auth_sessions.py` — Alembic migration +- `src/agentkit/server/auth/cache.py` — Redis session 元数据 cache + +**Backend(修改)**: +- `src/agentkit/server/auth/models.py` — 新增 AuthSessionModel +- `src/agentkit/server/auth/password.py` — 改密码时联动撤销 session +- `src/agentkit/server/dependencies.py` — `get_current_user` 校验 sid +- `src/agentkit/server/app.py` — 注册新路由 + +**Frontend(新增)**: +- `src/agentkit/server/frontend/src/api/tauri-auth.ts` — Tauri Keychain 桥接 +- `src/agentkit/server/frontend/src/api/sessions.ts` — sessions API 客户端 +- `src/agentkit/server/frontend/src/views/SettingsView.vue`(或扩展) — 「我的设备 / 修改密码」 + +**Frontend(修改)**: +- `src/agentkit/server/frontend/src/stores/auth.ts` — 三态启动、Keychain 集成、预刷新、remember_me +- `src/agentkit/server/frontend/src/api/base.ts` — 预刷新拦截器、refresh reuse 失败处理 +- `src/agentkit/server/frontend/src/views/LoginView.vue` — 「记住我」checkbox +- `src/agentkit/server/frontend/src/App.vue` — 启动三态路由 + +**Tauri(新增)**: +- `src/agentkit/server/frontend/src-tauri/Cargo.toml` — 加 `keyring = "3"` +- `src/agentkit/server/frontend/src-tauri/src/auth.rs` — Keychain commands +- `src/agentkit/server/frontend/src-tauri/src/lib.rs` — 注册 commands +- `src/agentkit/server/frontend/src-tauri/capabilities/default.json` — 加 permissions + +**Tauri(修改)**: +- 无(仅新增) + +### 7.2 数据流 + +#### 启动流程(Tauri 桌面) + +``` +[Window opens] + ↓ +[WebView 加载 → App.vue 启动] + ↓ +[bootstrapBackend] → start_backend → check_health + ↓ +[auth.startupCheck()] + ├── 1. 从 Keychain 读 refresh_token + ├── 2. 调 GET /api/v1/auth/whoami 携带 refresh_token + │ ├─ 200 → valid 状态,user 写入内存 + localStorage 缓存 + │ ├─ 401 → invalid 状态,显示「会话过期」+ 重试按钮 + │ └─ 网络错 → error 状态,显示「无法连接」+ 重试按钮 + └── 3. 跳到 /agent 或 /login +``` + +#### 正常 API 请求(Tauri 桌面) + +``` +[Pinia store 请求拦截器] + ↓ +[检查 accessToken 剩余有效期] + ├── < 2min → silentRefresh() + │ ├── 调 POST /api/v1/auth/refresh + │ ├── 成功 → 新 access 进内存,新 refresh 进 Keychain + │ └── 失败 → 清内存,路由跳 /login + └── > 2min → 直接放行 + ↓ +[加 Authorization: Bearer ] + ↓ +[API 请求] + ├── 200 → 返回 + └── 401 → refreshIfPossible(最后一次兜底) + ├── 成功 → 重试原请求 + └── 失败 → 清状态,路由跳 /login +``` + +#### 启动流程(Web 浏览器) + +``` +[同 Tauri,但 refresh_token 走 localStorage] +[其余逻辑完全一致] +``` + +--- + +## 8. Open Questions / Assumptions + +### 8.1 假设 + +- **A1**: 用户当前没有 IdP 集成需求,集团统一管理仅靠本系统自带的 session 管理 + admin 端点满足 +- **A2**: 单租户架构不变(`auth.db` 全局共享) +- **A3**: Tauri 仅支持桌面平台(macOS / Windows / Linux),不规划移动端 +- **A4**: Keychain 失败时降级到 localStorage 可接受(dev 环境 / Linux 无 keyring daemon) +- **A5**: Web 端仅作为开发/降级用途,不追求 localStorage 加密 +- **A6**: 旧客户端灰度期 ≤ 1 个 minor 版本(约 30 天) + +### 8.2 待澄清(实施前确认) + +- **Q1**: 「记住我」延长到 30 天还是更长?30 天 vs 90 天 +- **Q2**: admin 端点路径用 `/api/v1/admin/users/{id}/sessions` 还是嵌在 `/api/v1/auth/admin/...`? +- **Q3**: session 列表 UI 放在「设置 → 安全」还是单独页面? +- **Q4**: 是否需要 session 数量上限(同一用户最多 N 个活跃 session)?默认 10? +- **Q5**: 改密码时是「全踢」还是「踢其他保留当前」?当前设计保留当前 +- **Q6**: refresh reuse 检测触发全踢后,是否给用户发邮件 / 站内通知?当前无邮件能力,先只写审计日志 + +--- + +## 9. Success Criteria + +- ✅ 首次登录后,Tauri 客户端冷启动直接进主界面,**不再显示登录页**(除非主动登出或 token 失效) +- ✅ Tauri 端 refresh token 不再出现在 `~/Library/WebKit/.../LocalStorage/` +- ✅ admin 端能在「用户管理」看任意用户的活跃 session 列表 +- ✅ 改密码后其他设备立即失效(≤ 1s 内下次请求 401) +- ✅ refresh token 被轮换后再用 → 全用户 session 撤销 + 审计记录 +- ✅ 「记住我」勾选后 refresh 30 天,不勾选 7 天 +- ✅ access token 剩余 < 2min 时主动 refresh,批量请求不出现 401 风暴 +- ✅ 旧客户端(1 个 minor 版本内)仍可登录 +- ✅ 所有新增代码测试覆盖率 ≥ 80% +- ✅ pytest 全部通过(包含新增 auth 集成测试) + +--- + +## 10. Out of Scope (Explicit) + +- 企业 IdP / SSO(OIDC / SAML / LDAP / 飞书 / 钉钉 / 企微)— 下一迭代 +- 2FA / TOTP / WebAuthn / Passkey — 后续 +- 多租户 — 后续 +- 密码强度策略 / 密码过期 — 后续 +- 登录失败锁定 / 滑窗限流 — 后续安全加固 +- 邮件 / 短信通知 — 需要先有通知服务 +- 完整审计日志 / 全文检索 / 导出 — 后续 + +--- + +## 11. References + +- 现状代码: + - [auth/models.py](file:///Users/chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/server/auth/models.py) + - [auth/password.py](file:///Users/chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/server/auth/password.py) + - [routes/auth.py](file:///Users/chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/server/routes/auth.py) + - [stores/auth.ts](file:///Users/chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/server/frontend/src/stores/auth.ts) + - [router/index.ts](file:///Users/chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/server/frontend/src/router/index.ts) + - [views/LoginView.vue](file:///Users/chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/server/frontend/src/views/LoginView.vue) + - [api/base.ts](file:///Users/chiguyong/Code/Fischer/fischer-agentkit/src/agentkit/server/frontend/src/api/base.ts) + +- 外部参考: + - `keyring` crate:https://docs.rs/keyring/latest/keyring/ + - OWASP JWT 安全备忘单:https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html + - Auth0 Refresh Token Rotation:https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation