docs: 集中鉴权与 Token 持久化需求文档
覆盖 A+B+C 一次到位方案: - A 当前实现加固(refresh 轮换、记住我、预刷新、启动三态) - B Tauri OS Keychain 集成(keyring crate 跨 macOS/Win/Linux) - C 服务端 Session 表(滑动过期、踢出、改密码强踢、reuse 检测) Out of scope: 企业 IdP / SSO / 2FA / 多租户(后续单独 brainstorm)
This commit is contained in:
parent
d245f2e3d8
commit
df8a995ec4
|
|
@ -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<Option<String>, 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<void> {
|
||||
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<string | null> {
|
||||
if (isTauri()) {
|
||||
try {
|
||||
return await invoke<string | null>('load_refresh_token')
|
||||
} catch (e) {
|
||||
console.warn('Keychain 读取失败,fallback', e)
|
||||
}
|
||||
}
|
||||
return localStorage.getItem('agentkit.refresh_token')
|
||||
},
|
||||
async clearRefreshToken(): Promise<void> {
|
||||
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 <access>]
|
||||
↓
|
||||
[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
|
||||
Loading…
Reference in New Issue