OpenHuman 凭据管理模块深入解析:app-session 会话生命周期与 auth-profiles 加密存储
2026/9/10 7:51:19 网站建设 项目流程

OpenHuman 凭据管理模块深入解析:app-session 会话生命周期与 auth-profiles 加密存储

【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhuman

credentials是 OpenHuman 的凭据与认证中枢,统一管理三件事:应用会话(app-session)JWT 的登录/登出生命周期、各 Provider(如 API Key、OAuth Token 集)的磁盘凭据档案(auth-profiles),以及后端 OAuth 连接/交接与 Composio 直连模式(BYO Key)凭据槽。本文以 src/openhuman/security/credentials/README.md 为骨架,结合模块源码逐层拆解其数据结构、加密持久化机制、RPC/CLI 接口与事件驱动拆解流程,帮助读者掌握该模块的完整工作原理与排查要点。

模块定位与职责边界

该模块(源码位于 src/openhuman/security/credentials/)对外只暴露auth.*JSON-RPC / CLI 命名空间,所有能力都经由AuthService门面与AuthProfilesStore持久化引擎落地。其核心职责可概括为以下七个方面:

  • 会话 JWT 管理:存储并校验app-sessionProvider(default档案)的会话令牌,包括本地离线会话与后端GET /auth/me校验。
  • 登录编排:激活用户级 OpenHuman 目录、首次激活时清理登录前(匿名)会话线程、绑定记忆/会话持久化、引导 subconscious,并启动登录门控服务(本地 AI、语音、听写、自动补全)。
  • 登出/会话过期拆解:移除 JWT、清除活跃用户标记、停止登录门控服务、重置 subconscious,并翻转调度器(scheduler-gate)的 signed-out 覆盖位。
  • Provider 凭据档案:以命名档案(named auth profiles)形式持久化任意 Provider 的凭据(token + 元数据字段),支持列出、删除、设置活跃档案,以及按前缀列出分组命名空间(如channel:*)。
  • 后端 OAuth 流程:连接 URL 生成、集成列表、交接令牌(handoff token)获取、一次性客户端密钥获取、集成吊销。
  • Composio 直连模式:存取/清除 Composio 直连 API Key(composio-directProvider)。
  • 通用加解密:通过SecretStore加密/解密任意机密。

核心文件地图

文件职责
mod.rs模块出口。重导出core::*ops(同时以rpc别名导出)、Composio-direct 辅助函数、schema 控制器(all_credentials_controller_schemas/all_credentials_registered_controllers)及crate::api::rest的后端 OAuth REST 类型。
core.rsAuthService门面,封装AuthProfilesStore——store/get/remove/set-active 档案、解析 Bearer Token、profile-id 选择逻辑(override → active → default → 同 Provider 任意档案)、Provider 规范化、state 目录推导。
profiles.rs持久化引擎。定义AuthProfile/TokenSet/AuthProfileKind/AuthProfilesDataAuthProfilesStore——原子 JSON 读写、keychain 与加密 JSON 双轨密钥处理、旧版密文迁移、损坏档案隔离、PID 感知的陈旧锁恢复。
ops.rs(拆分为 ops_part_01.rs、ops_part_02.rs)业务逻辑 + RPC 入口(返回RpcOutcome<T>)。会话生命周期、登录/登出服务编排、Provider 凭据 CRUD、OAuth 流程、Composio-direct 密钥辅助、机密加解密。以rpc别名重导出。
schemas.rsauth.*控制器 schema 与handle_*分发器,委托给ops。定义all_controller_schemas/all_registered_controllers
session_support.rs会话/认证辅助:build_session_stateget_session_tokenload_app_session_profilesummarize_auth_profile、本地会话检测/slug、字段解析。RPC 与 HTTP host 共用。
responses.rs响应 DTO:AuthStateResponseAuthProfileSummary
cli.rsCLI 认证入口(cli_auth_login/logout/status/list),按app-session与 Provider 分流;解析--field key=value
bus.rsSessionExpiredSubscriber——DomainEvent::SessionExpiredEventHandler,执行规范化的会话拆解。

配套测试以#[path = ...]方式内嵌在 ops_tests.rs、profiles_tests.rs、schemas_tests.rs 中,同时core.rssession_support.rsbus.rscli.rs内均有#[cfg(test)]内联测试。

数据结构与公共 API

核心类型

  • AuthService(core.rs):唯一的对外门面,方法包括from_confignewload_profilesstore_provider_tokenset_active_profileremove_profileget_profileget_provider_bearer_token
  • AuthProfile(profiles.rs):单个凭据档案,字段为idprovider:profile_name拼接)、providerprofile_namekindOAuth/Token)、account_idworkspace_idtoken_set(OAuth 令牌集)、token(Token 类令牌)、metadata(有序BTreeMap)、created_at/updated_at
  • TokenSet:OAuth 令牌集,含access_tokenrefresh_tokenid_tokenexpires_attoken_typescope;并提供is_expiring_within(skew)预判过期。
  • AuthProfileKind:序列化为 kebab-case 的oauth/token两种类型,由parse_profile_kind严格解析,未知值直接报错丢弃。
  • AuthProfilesData:磁盘数据整体,含schema_version(当前为 1)、updated_atactive_profiles(provider → profile-id)、profiles(id → profile)。
  • AuthProfilesStore:持久化引擎,见下一节。

关键常量

  • APP_SESSION_PROVIDER = "app-session"DEFAULT_AUTH_PROFILE_NAME = "default"(core.rs)
  • COMPOSIO_DIRECT_PROVIDER = "composio-direct"(定义于 ops)
  • LOCAL_SESSION_USER_ID = "local"(session_support.rs)
  • SESSION_EXPIRES_AT_META = "session_expires_at"(会话档案元数据中记录的 JWTexpRFC3339 键)、SESSION_EXPIRY_SKEW_SECS = 30(提前 30 秒判定过期)

档案选择与 Provider 规范化

normalize_provider会将 Provider 名 trim 并转为小写,空字符串直接报错。select_profile_id(core.rs)的解析优先级严格为:显式 override → active 档案 →default档案 → 该 Provider 的任意档案resolve_requested_profile_id支持两种写法:包含:的字符串视为完整档案 id(如channel:telegram),否则按provider:name拼接。

磁盘持久化:auth-profiles.json 与密钥处理

AuthProfilesStore将凭据写入配置 state 目录(config.config_path的父目录,登录后切换为用户级目录)下的auth-profiles.json(文件名常量见 profiles.rs)。整体布局为:schema_version(当前 = 1)、updated_atactive_profiles(provider → profile-id)、profiles(id → profile)。

双轨密钥存储

机密字段(token / access_token / refresh_token / id_token)的落盘方式按运行环境自动切换:

  • OS 系统钥匙串(keychain)优先:当crate::openhuman::security::keyring::is_available可用时,所有 token 字段以单个 keychain 条目存储,key 格式为{user_id}:auth:{profile_id},其中user_id从 state 目录推导(典型路径~/.openhuman/users/uid-123取末段为uid-123,异常布局则退化为路径的 FNV-1a 哈希path-{hex},见 profiles.rs)。此时 JSON 中不含任何机密字段
  • 加密 JSON 回退:在无头/CI 等无钥匙串环境下,token 字段经SecretStoreChaCha20-Poly1305加密后写入 JSON。

文件权限与原子写

加密 JSON 是 Linux 及无钥匙串安装的常态,为防他人同机可读,写入走write_owner_only(profiles.rs):Unix 下以0o600模式创建文件并显式set_permissions,避免fs::write默认的0644权限;写临时文件后经 rename 原子替换。写与 rename 两个阶段各有 6 次重试预算(PERSIST_RETRY_ATTEMPTS = 6、退避基址 100ms,整体约 6.2s 最坏耗时,仍远小于锁超时)。

崩溃安全与锁文件

所有变更由auth-profiles.lock守卫,该锁文件带有 PID 标记,三层回收机制防卡死:

  • 进程内注册表(in_process_lock_for)按规范化路径为每个锁文件维护一个'static Mutex,别名路径(相对/绝对、符号链接、Windows 大小写变体)共享同一把进程内锁,避免同进程第二个获取者把活锁误判为泄漏。
  • 跨进程陈旧锁回收:STALE_LOCK_AGE_MS = 30_000(超过即视为泄漏)、无 PID 的畸形锁MALFORMED_LOCK_GRACE_MS = 2_000快速回收、LOCK_TIMEOUT_MS = 35_000兜底。锁的释放Drop中带 5 次重试卸载(应对 Windows AV/索引器短暂占用句柄),失败则交由自持锁回收与年龄回收兜底——这是修复"杀死进程后重开卡在 Initializing OpenHuman 约 30 秒"的关键机制。
  • 磁盘满(StorageFull/ReadOnlyFilesystem)导致锁创建失败时,读路径可以安全跳过排他锁(写方本就原子发布),其余错误照常上抛保持可见。

迁移、隔离与自我修复

  • 加载时会迁移旧版enc:/enc2:密文字段,并将机密提升(promote)进系统钥匙串。
  • 无法解密或kind非法的单个档案会被丢弃而非毒化整个存储
  • 无法解析的整体文件会被隔离(quarantine)重命名为auth-profiles.corrupt-<ts>.json并重置为空存储,保证应用可继续启动。

app-session 会话生命周期

会话令牌作为app-sessionProvider 的default档案存储。注意 README 的明确警告:store_session远不止存一个 token——它同时负责目录激活、线程清理、服务启动,是整个登录漏斗,而非薄 setter。

登录编排

登录时依次执行:激活用户级 OpenHuman 目录(user_openhuman_dir/write_active_user_id等)、首次激活清除登录前(匿名)会话线程、绑定记忆与会话持久化、引导 subconscious、启动登录门控服务。存储时会用GET /auth/me校验后端用户档案(带user_id_from_auth_me_payload解析),并记录session_expires_at元数据。校验有独立预算(默认 12s,可通过环境变量OPENHUMAN_AUTH_ME_STORE_TIMEOUT_MS覆盖),对 408/429/500/502/503/504/520 等瞬态状态做 150ms 间隔重试,避免慢后端拖垮桌面端登录 RPC 的前端超时预算(对应 SentryTAURI-REACT-1V的修复)。嵌入宿主(libraryHarness)环境下则完全绕开全局~/.openhuman/active_user.toml,状态全部限定在自身Config路径下。

本地离线会话

本地会话完全由 JWT 签名段字面量为local判定(is_local_session_token,session_support.rs:恰好四段且第三段为local)。此类会话跳过后端校验、永远不会被判定为过期,用户 id 为local-{hostname-slug}(主机名小写化并 slug 化,见local_session_user_id)。

会话过期检测与 401 拦截

require_live_session_token(session_support.rs)是每个后端authed_json调用的标准闸门,三种结果:

  • 无令牌 →"no backend session token"错误(本地离线,不联网)。
  • 令牌记录的exp已过(含 30 秒提前量)→在发起注定 401 的请求之前发布一次SessionExpired事件(经调度器门去重,N 个并行调用只发一条),并返回SESSION_EXPIRED哨兵错误。
  • exp记录的令牌(本地离线 / 无 exp JWT)→ 仅做存在性检查,服务器端吊销仍由统一的 401 扁平化网络兜底。

该机制是 TAURI-RUST-8WY / 8WZ(/teams/me/usage/payments/stripe/currentPlan的 401 洪泛)的根因修复:把 401 在源头掐断,而不是事后降级。

登出与拆解

clear_session移除 JWT、清除活跃用户标记、停止登录门控服务(语音服务器、本地 AI 复位为 idle 但不杀掉 Ollama 进程——它可能服务其他客户端或正在下载模型)、重置 subconscious,并翻转调度器 signed-out 覆盖位。

登录门控服务编排

start_login_gated_services/stop_login_gated_services(ops_part_01.rs)并发启动相互独立的服务,唯一顺序约束是语音服务器必须先于独立听写监听器(二者争抢 macOS 上唯一的 rdev 全局监听器):

  1. 本地 AI(Ollama、embeddings)——最重的预热项,被移出关键路径;
  2. 语音热键(voice server + 按需独立听写监听);
  3. always-on 持续监听(连续麦克风 + VAD → STT → Agent,其 Windows WASAPI 冷启动为阻塞型就绪握手)。

此前串行启动使冷启动耗时叠加(本地 AI 引导 + Windows 麦克风初始化叠加出约 10 秒的卡顿,热键/命令注册还被挤到 Ollama 预热之后),#3490 改为并行后,就绪时间由最慢单服务决定而非总和。单元测试下默认跳过真实后台服务(cfg!(test)编译期移除),需要时用OPENHUMAN_RUN_LOGIN_GATED_SERVICES_IN_TEST显式开启。

auth.* RPC / CLI 接口

命名空间auth(JSON-RPCopenhuman.auth_*/ CLI),schema 定义于 schemas.rs:

方法说明
auth_store_session存储并校验 app-session JWT。
auth_clear_session移除已存的 app-session 凭据。
auth_get_state当前认证/会话状态(AuthStateResponse)。
auth_get_session_token读取已存的 app-session 令牌。
auth_get_me获取当前已认证的后端用户档案。
auth_consume_login_token消费一次性登录交接令牌 → 会话 JWT。
auth_create_channel_link_token生成短时渠道链接令牌(telegram/discord)。
auth_store_provider_credentials为某档案存储 Provider 凭据。
auth_remove_provider_credentials移除 Provider 凭据。
auth_list_provider_credentials列出已存 Provider 凭据(可选 Provider 过滤)。
auth_oauth_connect为 Provider 创建 OAuth 连接 URL。
auth_oauth_list_integrations列出当前会话的 OAuth 集成。
auth_oauth_fetch_integration_tokens获取集成交接令牌。
auth_oauth_fetch_client_key获取加密集成的一次性客户端密钥共享。
auth_oauth_revoke_integration吊销 OAuth 集成。

list_provider_credentials_by_prefix与 Composio-direct/secret 辅助函数是公开 ops 但未注册为auth.*控制器——由其他域直接调用。

CLI 侧

cli.rs 提供cli_auth_login/logout/status/list,统一先load_config_with_timeout,再按provider == "app-session"分流到会话流或 Provider 凭据流。--field key=valueparse_field_equals_entries解析为 JSON 对象(缺失=、空 key 均报错),支持以--profile指定档案名、--set-active控制是否置为活跃。

SessionExpired 事件订阅

SessionExpiredSubscriber(bus.rs,name() == "credentials::session_expired_handler",域过滤["auth"])订阅 src/core/events.rs 中的DomainEvent::SessionExpired(发布方是各 401 检测点,如jsonrpc.invoke_methodllm_provider.api_error等),执行规范拆解:

  1. 翻转调度器 signed-out 覆盖位——所有后台 worker 会在下一次wait_for_capacity()处停摆,不再向必然 401 的后端发请求(且拆解期间重入闸门的任务同样停摆);
  2. 再调用clear_session移除 JWT、清除活跃用户标记、停止登录门控服务。重复事件安全(幂等)。

本地离线会话则撤销第一步的翻转并 no-op。该订阅者是 issueOPENHUMAN-TAURI-1T(一名用户会话过期后由 cron 驱动的 LLM 调用产生了 5,414 条 Sentry 事件)的修复——没有它,401 只会被检测而不会被处置,下一轮循环继续 401。bus.rs的注释同时说明:若拆解时配置加载失败,调度器门仍被钉在 signed-out,会话 JWT 本周期不清理,但至少后台工作不会恶化局面。

后端 OAuth 连接与交接

OAuth 相关类型从crate::api::rest重导出:BackendOAuthClientConnectResponseIntegrationSummaryIntegrationTokensHandoffdecrypt_handoff_blobuser_id_from_auth_me_payloaduser_id_from_profile_payload。整体流程为:auth_oauth_connect取得连接 URL → 用户完成授权 →auth_consume_login_token消费一次性交接令牌换会话 JWT →auth_oauth_fetch_integration_tokens取交接令牌集 → 需要时auth_oauth_fetch_client_key取一次性客户端密钥共享并decrypt_handoff_blob解密集成凭据 →auth_oauth_revoke_integration吊销。会话令牌通过crate::api::jwt读取,后端地址由crate::api::config::effective_backend_api_url解析。

Composio 直连模式(BYO Key)

composio-directProvider 提供三个直接辅助函数:store_composio_api_keyget_composio_api_keyclear_composio_api_key,另有 RPC 包装rpc_store_composio_api_key。调用方(composio/{client,ops}.rs)通过credentials::rpc使用该槽位实现自带 API Key 的直连模式。

依赖关系与消费方

模块依赖(README 的 Dependencies 一节):config(Config、配置加载、用户目录激活、onboarding 状态)、security::keyringSecretStore加解密 + 系统钥匙串)、cron::scheduler_gate(登录/登出/过期翻转 signed-out 覆盖)、memory::conversations(清理 pre-login 线程、绑定会话持久化)、memory(登录后绑定记忆客户端)、subconscious(登录后引导 / 用户切换重置)、inference/voice/autocomplete(登录门控服务)、api::config/api::jwt/api::rest(后端地址、令牌读取、OAuth 客户端)、core(控制器注册表 + RPC 信封 + 事件总线)。

消费方众多:src/core/{all,auth,jsonrpc}.rs(控制器接线与认证闸门)、src/api/jwt.rsapp_state/ops.rs(会话快照)、channels/*(受管凭据)、composio/*(BYO Key)、config/schema/*embeddings/cloud.rsencryption/ops.rshttp_host/auth.rsinference/*(Provider 认证、OpenAI OAuth)、migrations/unify_ai_provider_settings.rsreferral/ops.rssubconscious/engine.rswebhooks

注意事项与易踩坑点

  • rpc.rs例外mod.rs同时以ops::*pub use ops as rpc导出,调用方统一走credentials::rpc::*,这是文档化的"rpc.rs 等价物"例外——不存在独立的rpc.rs文件
  • 历史路径引用responses.rs/session_support.rs注释中引用的crate::core_server是历史路径,实际传输层 crate 是src/core/
  • store_session是登录漏斗:做了目录激活、线程清理、服务启动等重编排,不要当薄 setter 使用。
  • 本地会话判定:只看 JWT 签名段是否字面量为local,跳过后端校验且永不判过期。
  • 密钥永不落日志:调试日志只记录长度/标记,遵守 CLAUDE.md 的脱敏规则;AuthProfileSummary也刻意只暴露metadata_keys(排序后的键名)、has_token/has_token_set布尔位,而非任何令牌明文。
  • GET /auth/me校验预算:存储时校验上限默认 12 秒,慢后端会快速失败进入"调用方授权的挂起会话"路径(对 live-exp JWT),而不是把已认证用户弹回登录页。
  • Agent 工具声明与源码现状:README 声明该模块"不拥有 Agent 工具"(tools.rs不存在),但从当前源码目录结构看,已存在 tools.rs 与 tools_tests.rs 文件,可以推断 README 的该声明可能已滞后于代码演进,具体以源码为准。

小结

OpenHuman 的credentials模块是典型的"表面简单、内部严谨"的认证基础设施:对外只有一组auth.*RPC/CLI 接口,对内则完成了双轨加密持久化(系统钥匙串 + ChaCha20-Poly1305 回退)、PID 感知的崩溃安全锁、损坏自愈、会话过期事件驱动的全链路拆解。理解它的档案选择优先级、本地会话判定与调度器闸门联动,是排查登录异常、401 洪泛与"卡在 Initializing OpenHuman"等问题的关键前提。

【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhuman

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询