fischer-agentkit/docs/brainstorms/2026-06-20-centralized-auth...

32 KiB
Raw Permalink Blame History

Fischer AgentKit — 集中鉴权与 Token 持久化 (Requirements)

Date: 2026-06-20 Branch: feat/auth-server-token-persistence(原 feat/centralized-auth-token-persistence Status: Active — 已合并 AuthProvider 抽象层 scope2026-06-20 更新) Scope: 服务端签发 JWT + 客户端安全持久化 + 服务端 Session 表 + Refresh Token 轮换 + 「记住我」 + 启动态区分 + AuthProvider 抽象层(为未来对接集团 IdP 留扩展点) Out of scope: 实现具体企业 IdP 适配OIDC / SAML / LDAP / 飞书 / 钉钉 / 企微、多租户、密码强度策略、2FA、SSO 跳转


1. Context & Problem

1.1 现状

已经实现(但不够健壮)

暴露的问题

  1. Token 持久化安全弱 — access + refresh + user 明文存 WebView localStoragemacOS: ~/Library/WebKit/.../LocalStorage/),被其他进程或恶意浏览器扩展可读
  2. 无 refresh token 轮换 — 同一 refresh token 可在 7 天内无限复用,被泄漏后无重置窗口
  3. 无服务端 Session 表 — 改密码 / 踢人 / 看活跃设备 全部做不到;user_sessions 表(auth/models.py:55-66)只存 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 三态
  • 鉴权后端 可插拔AuthProvider 抽象):当前 Local未来 OIDC / SAML / LDAP 只需新增 adapter
  • admin 端点 与认证后端解耦(统一通过 user_id 操作),未来切换 IdP 不影响 session 管理
  • 审计日志记录登录来源(auth_provider 字段),集团接管时可溯源

1.3 设计哲学

服务端权威 + 客户端最小信任 + 加密落盘 + 可观测可治理 + 认证后端可插拔

  • 服务端权威:所有 token 校验、续期、撤销由服务端说了算,客户端不可绕过
  • 客户端最小信任客户端不存密码、access token 不持久化仅内存、refresh token 进 Keychain
  • 加密落盘refresh token 走 OS 级加密Tauri: macOS Keychain / Windows Credential Manager / Linux Secret Service
  • 可观测可治理admin 能看 / 踢任意 session集团统一管理的基础设施准备好
  • 认证后端可插拔:所有用户认证逻辑走 AuthProvider Protocolauthenticate / get_user / sync_attributes / revoke_user当前 LocalAuthProvider 封装 SQLite + bcrypt未来 OidcAuthProvider 接管时路由层、admin API、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 仍可存 localStoragefallback 浏览器用户也能用,只是降级安全
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
F13 鉴权后端可插拔AuthProvider 抽象) 配置切换 localoidc-stub,路由/Admin/Session 表零修改
F14 admin 端点与认证后端解耦 未来切 IdPadmin 看 session 列表 / 踢人功能不变
F15 审计日志记录 auth_provider 字段 登录来源可溯源local / oidc / saml

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 兼容旧版本客户端的 JWT7d 滚动)至少 1 个 minor 版本 灰度期间双轨

3. Non-Goals

  • 实现具体企业 IdP 适配OIDC / SAML / LDAP / 飞书 / 钉钉 / 企微)— 下一迭代单独 brainstorm本次只预留 AuthProvider 抽象层
  • 多租户 / 集团多组织隔离 — 当前单租户架构不动
  • 密码强度策略 / 密码过期 / 密码历史 — 单独的 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 轮换的上一跳(用于审计)
auth_provider TEXT 登录来源:local / oidc-stub / saml(未来扩展)

5.1.2 JWT payload 扩展

{
  "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 轮换检测逻辑

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」set30s
    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

// 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 封装

// 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 预刷新逻辑

// 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 migrationadd_auth_sessions_table
  • user_sessionsrefresh 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 天灰度)

5.5 鉴权后端可插拔 — AuthProvider 抽象层

设计动机:当前用本地 users 表 + bcrypt 校验密码。未来集团对接 OIDC / SAML / LDAP 时路由层、admin API、Session 表都不应重写。通过 AuthProvider Protocol 把"用户存在哪里 / 密码怎么校验 / 属性怎么同步"封装在 adapter 内部。

5.5.1 AuthProvider Protocol

# auth/providers/base.py
from typing import Protocol
from ..models import User

class AuthProvider(Protocol):
    """所有鉴权后端必须实现的能力。

    路由层只调用以下方法,不感知具体实现是 SQLite / OIDC / LDAP。
    """

    name: str  # 标识当前 provider写入 session.auth_provider

    async def authenticate(self, *, username: str, password: str) -> User:
        """校验用户名 + 密码,返回 User 对象。失败抛 InvalidCredentials。"""
        ...

    async def get_user_by_id(self, user_id: int) -> User | None:
        """按 id 查 useradmin 端点、session 校验、whoami 都用这个)。"""
        ...

    async def sync_user_attributes(self, user_id: int) -> None:
        """同步用户属性(部门/邮箱/职位等)。

        LocalAuthProvider: no-op
        OidcAuthProvider:  从 IdP 拉最新 profile 写回本地 users 表
        """
        ...

    async def revoke_user(self, user_id: int) -> None:
        """禁用用户(离职 / 锁定场景)。

        LocalAuthProvider: UPDATE users SET is_active=0
        OidcAuthProvider:  调 IdP 的 disable API未来
        """
        ...

5.5.2 默认实现 LocalAuthProvider

# auth/providers/local.py
class LocalAuthProvider:
    name = "local"

    def __init__(self, db: aiosqlite.Connection):
        self._db = db

    async def authenticate(self, *, username: str, password: str) -> User:
        # 封装现有 routes/auth.py:201-213 的 password 校验逻辑
        row = await self._db.execute(
            "SELECT id, username, password_hash, is_active FROM users WHERE username = ?",
            (username,),
        )
        row = await row.fetchone()
        if not row or not row["is_active"]:
            raise InvalidCredentials("user not found or inactive")
        if not verify_password(password, row["password_hash"]):
            raise InvalidCredentials("invalid password")
        return await load_user(row["id"])

    async def get_user_by_id(self, user_id: int) -> User | None:
        return await load_user(user_id)

    async def sync_user_attributes(self, user_id: int) -> None:
        return  # local provider: no-op

    async def revoke_user(self, user_id: int) -> None:
        await self._db.execute(
            "UPDATE users SET is_active = 0 WHERE id = ?", (user_id,)
        )
        await self._db.commit()

5.5.3 占位实现 StubOIDCProvider

# auth/providers/oidc_stub.py
class StubOIDCProvider:
    """OIDC 对接的接口占位。

    当前阶段只定义接口契约,不做实际 IdP 通讯。下一迭代实现时,
    重写 authenticate / sync_user_attributes / revoke_user 即可,
    路由层、admin API、Session 表零修改。
    """

    name = "oidc-stub"

    async def authenticate(self, *, username: str, password: str) -> User:
        raise NotImplementedError(
            "OIDC provider not implemented. "
            "Use 'local' provider in agentkit.yaml: auth.provider: local"
        )

    async def get_user_by_id(self, user_id: int) -> User | None:
        raise NotImplementedError

    async def sync_user_attributes(self, user_id: int) -> None:
        raise NotImplementedError

    async def revoke_user(self, user_id: int) -> None:
        raise NotImplementedError

5.5.4 Provider 切换配置

# agentkit.yaml
auth:
  provider: local  # local | oidc-stub  (未来: oidc-keycloak, oidc-feishu, ...)
  session:
    table: auth_sessions
    access_ttl_seconds: 900
    refresh_ttl_seconds: 604800
    refresh_ttl_remember_me_seconds: 2592000
  jwt:
    secret_env: AGENTKIT_JWT_SECRET
    algorithm: HS256

5.5.5 路由层 DI 注入

# auth/providers/__init__.py
from functools import lru_cache
from ..config import get_settings
from .base import AuthProvider
from .local import LocalAuthProvider
from .oidc_stub import StubOIDCProvider

@lru_cache
def get_auth_provider() -> AuthProvider:
    settings = get_settings()
    db = await get_auth_db()  # 现有 aiosqlite 连接
    if settings.auth.provider == "local":
        return LocalAuthProvider(db)
    elif settings.auth.provider == "oidc-stub":
        return StubOIDCProvider()
    else:
        raise ValueError(f"unknown auth provider: {settings.auth.provider}")
# routes/auth.py 改造点
from fastapi import Depends
from ..auth.providers import get_auth_provider, AuthProvider

@router.post("/login")
async def login(
    body: LoginRequest,
    provider: AuthProvider = Depends(get_auth_provider),
) -> LoginResponse:
    user = await provider.authenticate(username=body.username, password=body.password)
    # ... 后续 session 创建逻辑不变

5.5.6 admin 端点与 Provider 解耦

所有 admin API/admin/users/{id}/sessions 等)都通过 user_id 操作,不直接调用 provider.authenticate。这意味着:

  • 未来切到 OIDCadmin 踢人 / 看 session 列表功能不变
  • LocalAuthProvider.revoke_user 和 OidcAuthProvider.revoke_user 实现不同,但 admin 端点统一调 provider.revoke_user(user_id)
  • 审计日志记录 auth_provider,未来切 IdP 后可溯源"哪个 session 是本地建的、哪个是 IdP 建的"

5.5.7 未来 IdP 对接清单(下一迭代参考)

下一迭代实现 OIDC 时,按此 checklist 推进:

  • auth/providers/oidc.py — 实现 OidcAuthProviderauthenticate / get_user / sync_attributes / revoke_user
  • auth/oauth_routes.py/auth/oauth/{provider}/redirect/auth/oauth/{provider}/callback 端点
  • auth/state_cache.py — OAuth state 参数防 CSRFRedis TTL 5min
  • 用户首次从 IdP 登录时的「本地账号创建」策略justeer / 拒绝 / 邀请制)
  • IdP 端的 session 同步IdP 登出时本地 session 也撤销)
  • 集团部门 / 职位属性映射到本地 users 表

本次迭代只做 1-3 项的占位(接口 + stub 实现),其余列入下一迭代的独立 brainstorm。


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 15minrefresh 7d / 30dremember_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 < 5msRedis cache session 元数据)
Session 列表查询 P99 < 50msuser_id 索引)
Redis cache TTL 60ssession 元数据)
Keychain 读写 < 10msmacOS Keychain 实际 1-3ms

6.3 兼容

要求
旧客户端 至少 1 个 minor 版本仍可登录(带 sid 字段的 JWT 也认)
Web 端 localStorage fallbackKeychain 不可用时降级
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/auth/providers/base.pyAuthProvider Protocol 接口契约
  • src/agentkit/server/auth/providers/local.pyLocalAuthProvider 默认实现(封装 SQLite + bcrypt
  • src/agentkit/server/auth/providers/oidc_stub.pyStubOIDCProvider 占位实现NotImplementedError + 文档)
  • src/agentkit/server/auth/providers/__init__.pyget_auth_provider() DI 工厂
  • src/agentkit/server/routes/auth.py — 修改 login/refresh/logout新增 whoami/sessions/change-password通过 Depends(get_auth_provider) 注入 provider
  • src/agentkit/server/routes/admin.py(或 auth 内部)— admin session 管理(按 user_id 操作,与 provider 解耦)
  • 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.pyget_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: 本次迭代只预留 AuthProvider 抽象层(接口 + Local + OIDC stub不实现具体 IdP 适配;集团对接需求在下一迭代独立 brainstorm
  • A2: 单租户架构不变(auth.db 全局共享)
  • A3: Tauri 仅支持桌面平台macOS / Windows / Linux不规划移动端
  • A4: Keychain 失败时降级到 localStorage 可接受dev 环境 / Linux 无 keyring daemon
  • A5: Web 端仅作为开发/降级用途,不追求 localStorage 加密
  • A6: 旧客户端灰度期 ≤ 1 个 minor 版本(约 30 天)
  • A7: AuthProvider 的 Local 实现保留 bcrypt 成本 = 12已实现未来 IdP 接管时 Local 仍可用作「本地应急账号」
  • A8: admin API 鉴权走现有 RBAC不依赖 provider未来切 IdP 时 admin 角色定义保持不变

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 / 飞书 / 钉钉 / 企微)— 下一迭代单独 brainstorm本次只预留 AuthProvider 抽象层
  • OAuth / SAML 跳转流程、state cache、用户属性同步等 IdP 集成细节 — 见 5.5.7 checklist
  • 2FA / TOTP / WebAuthn / Passkey — 后续
  • 多租户 — 后续
  • 密码强度策略 / 密码过期 — 后续
  • 登录失败锁定 / 滑窗限流 — 后续安全加固
  • 邮件 / 短信通知 — 需要先有通知服务
  • 完整审计日志 / 全文检索 / 导出 — 后续

11. References