Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

IDaaS 身份标准化终态方案

auth 作为 OIDC 基础服务,给下游业务(ExoMind/wechat/…)提供身份。本文是终态方案——抓住当前仅 2 个下游的窗口期,一次性落地,零过渡债。 定稿 2026-07-11。决策经评审确认:①一步到位不分阶段 ②tenant→org 彻底 rename ③scope 系统预置+业务申请制。


0. 背景与根因

ExoMind wiki 空白:下游把 auth 的 tenant_id(SaaS 租户)误用为用户身份做个人知识库 key。深层根因不是 tenant_id 的错,而是 auth 没给下游「标准、稳定、明确」的用户身份字段——sub 填的是 username(可变,下游不敢当主键)、user_id 是非标准字段名。下游找不到可信标识 → 乱抓字段 → 抓到 tenant_id → 串用户。

这是 IDaaS 身份语义的系统性问题。当前仅 2 个下游(ExoMind/wechat),是建标准的黄金窗口——一次终态,成本最低,未来 N 个下游时不还债。


1. 业界调研结论(Auth0/Keycloak/Azure 共性)

维度业界做法来源
字段暴露scope 驱动 claim(openid/profile/email/业务 scope 各对应一组 claim)Auth0 ActionsKeycloak client scopesAzure optional claims
per-client 配置每个 client 配自己的 scope/claim(Keycloak client scopes / Auth0 Actions 按 client_id / Azure app registration)scope vs roles
多租户单部署多 Organization(逻辑隔离),趋势替代每租户独立 realmAuth0 OrganizationsKeycloak 26+ Organizations
用户标识sub = user_id(OIDC 标准,稳定唯一)Auth0/Keycloak/Azure 一致

最稳定的锚点:OIDC 标准 claim(sub/iss/aud/exp/iat/jti)+ scope→claim 映射 + per-client 配置 + Organization 多租户。抓住这五点,十年不变。


2. 四层正交模型(架构地基)

业界能长期稳定,核心是四层解耦,互不污染

身份(Who)    sub = user_id     人/服务的唯一标识,跨组织/时间稳定(改名/换公司都不变)
组织(Whose)  org_id            身份的分组容器(个人/团队/企业),可变、可多属
权限(What)   permissions+scope 能做什么(RBAC 权限 + 客户端范围)
客户端(Which)client_id (aud)   哪个应用在请求,决定可见字段 + 权限边界

铁律:四者正交。token 是它们在「某次请求」的投影——按 client(aud) + 用户请求的 scope,投影出该次该下游该看到的字段。任一层变化不污染其他层。

ExoMind 错误的本质:把第二层(组织 org_id)当第一层(身份 sub)用——层级错位。根治 = 让 sub 标准可靠 + 用 scope 让它根本看不到 org_id。


3. 终态决策(已确认)

决策选择理由
落地节奏一步到位(不分阶段、无 sub_uid 过渡)仅 2 下游联调成本极低;中间态无架构价值,只增维护成本与割接风险
tenant→org彻底 rename(DB 列 + 代码,不只文档)一次终态不存历史错误语义;根除「tenant=用户身份」误导;改动量小,技术债清零
scope 治理系统预置 + 业务申请制 + 两表追踪系统级(openid/profile/email/auth)统一预置保证标准;业务自定义走申请制,可审计可追踪可灰度下线

4. token 设计(四层投影)

核心层(所有 token 必有,OIDC 协议级,不可删)

sub(user_id)   iss   aud(client_id)   exp   iat   jti

扩展层(按 scope 暴露,per-client 配置)

scopeclaim谁该要
openid(默认)sub所有下游
profilepreferred_username, name, picture要展示用户名的
emailemail, email_verified要邮箱的
auth:permissionspermissions[]要做 RBAC 授权的
auth:orgorg_id(当前组织)要做团队/企业隔离的(ExoMind 个人 wiki 不要这个 → 拿不到 org_id → 没机会误用
业务自定义(exomind:wiki 等)业务约定 claim特定业务,申请制

关键字段语义(写进 INTEGRATION.md,下游契约)

  • sub = 用户身份主键(下游用它做用户级隔离:个人知识库、配额、偏好)。稳定唯一,跨组织不变。
  • org_id = 组织容器(团队/企业数据隔离)。不是用户身份。个人场景不需要。
  • aud = 目标客户端(下游验 token 时校验 aud == 自己 client_id,防跨业务误用)。

5. 数据模型

5.1 tenant → org 彻底 rename(DB 列 + 代码)

所有表的 tenant_id 列 → org_id

  • users.org_idroles.org_idapi_keys.org_idaudit_logs.org_iddepartments.org_idoauth_clients.org_idtenant_oidc_providersorg_idp_providers(org 级 IdP)。
  • tenants 表 → organizations(org 主表)。
  • 代码:tenant_idorg_id(models/middleware/handlers/admin/sso/tenant.rs→org.rs 全局,grep 替换 + 编译引导)。
  • 当前数据:default org(个人),无企业。

5.2 client_scopes + oauth_client_scopes(新增,per-client + 追踪)

-- 可复用 scope 定义(scope → claim 映射束,Keycloak client scope 模式)
CREATE TABLE client_scopes (
    id INTEGER PRIMARY KEY,
    scope VARCHAR(50) UNIQUE NOT NULL,        -- openid/profile/email/auth:permissions/auth:org/exomind:wiki
    description VARCHAR(200),
    claims TEXT NOT NULL DEFAULT '[]',         -- JSON: ["preferred_username","name","picture"]
    is_system BOOLEAN DEFAULT FALSE,           -- 系统内置不可删
    created_at, updated_at
);
-- 系统 scope 预置(seed):openid(→sub) / profile / email / auth:permissions / auth:org

-- 每 OAuth client 启用哪些 scope(per-client 配置 + 追踪依据)
CREATE TABLE oauth_client_scopes (
    client_id VARCHAR(64) NOT NULL,            -- oauth_clients.client_id
    scope VARCHAR(50) NOT NULL,
    PRIMARY KEY(client_id, scope)
);

追踪查询(「谁在用哪些字段」+ 影响评估):

SELECT cs.scope, cs.claims, GROUP_CONCAT(ocs.client_id) AS downstream
FROM client_scopes cs JOIN oauth_client_scopes ocs ON cs.scope=ocs.scope
WHERE cs.claims LIKE '%email%' GROUP BY cs.scope;

6. 实施 plan(代码级,一次发版)

模块 1:身份标识标准化(治本)

  • src/auth.rs Claims:sub = user_id(string);加 preferred_username;加 aud = client_id。
  • 所有签 token 处(login/register/refresh/OAuth grant/SSO callback)填新 claims。
  • username 从 sub 移到 preferred_username(OIDC 标准 claim)。

模块 2:tenant → org 彻底 rename

  • DB 迁移(db.rs):所有表 tenant_id 列 rename org_id(SQLite 不支持直接 rename column,用「新建列 + 迁数据 + 旧列留空兼容」或重建表;或若可接受,保留列名 tenant_id 但代码层全用 org_id——决策:彻底 rename,重建表迁移)。
  • tenantsorganizations 表 rename。
  • 代码全局 tenant_idorg_id:models.rs / middleware.rs(RequestMeta) / admin.rs / handlers.rs / sso.rs / rbac.rs / tenant.rs→org.rs / oauth_client.rs。
  • admin-ui:tenant 文案 → 组织(tenant_id → org_id 字段)。

模块 3:scope + per-client claim 注入

  • db.rs:建 client_scopes + oauth_client_scopes 表 + seed 系统 scope。
  • models.rs:ClientScope / OAuthClientScope struct。
  • 签 token 逻辑:按 client(aud) 启用的 scope ∩ 用户请求 scope → 注入对应 claim(核心层始终有 + 扩展层按 scope)。
  • admin.rs:scope CRUD(系统 scope 不可删,业务 scope 申请制)+ client scope 配置 API。
  • admin-ui:oauth_clients 配置页加「scope 配置」+ 「下游字段用量」追踪视图。

模块 4:/userinfo + /oauth/token 规范化

  • /userinfo 返 sub(user_id) + 按 scope 的 claims(标准 OIDC userinfo)。
  • /oauth/token 响应 token 含 aud + 按 scope。
  • OIDC discovery(/.well-known)scopes_supported 列系统 scope。

模块 5:INTEGRATION.md 下游接入规范

新建 docs/INTEGRATION.md

  • 下游注册 OAuth client + 声明 scope。
  • 用 sub 做用户主键(标准)。
  • org_id 仅团队/企业隔离(与 sub 正交)。
  • 验 token 校验 aud。
  • scope 申请流程。

模块 6:下游联调

  • ExoMind:改读 sub(user_id) 做知识库 key;声明 scope = openid + exomind:wiki(不要 auth:org)。
  • wechat:评估 sub 变化(username→user_id)影响 + 声明 scope。

7. 文件影响清单

文件改动
src/auth.rsClaims(sub/aud/preferred_username) + 签 token
src/models.rsClaims/User/Role/… tenant_id→org_id + ClientScope/OAuthClientScope
src/middleware.rsRequestMeta tenant_id→org_id
src/db.rs表列 rename org_id + organizations + client_scopes + oauth_client_scopes + seed
src/admin.rstenant→org + scope CRUD + client scope 配置
src/handlers.rstenant→org
src/sso.rstenant→org
src/tenant.rs → src/org.rsrename + tenant→org
src/oauth.rsaud + scope 注入 claim
src/oauth_client.rstenant→org
admin-ui/*tenant→org 文案 + scope 配置 UI + 追踪视图
docs/INTEGRATION.md新建下游接入规范

8. 风险 + 回滚

风险应对
tenant→org rename 全局(编译错引导,但量大)grep tenant 全替换 + cargo check 迭代;编译保证不漏
sub 从 username→user_id 破坏下游缓存仅 2 下游,联调前通知;过渡期 sub 同时含 user_id(旧下游读 username 的会拿到 user_id 字符串,需适配)—— 但决策是不过渡,直接切,下游联调
DB 列 rename(SQLite 限制)重建表迁移(CREATE org_id 列 + COPY + 旧表留或 DROP);或在代码层 rename(DB 列名暂留 tenant_id,代码用 org_id)—— 决策:彻底 rename(含 DB),用重建表迁移
下游联调不顺ExoMind + wechat 同步改 + 联调;auth 提供 /userinfo 兜底(下游可实时查)

回滚:若联调失败,回退 commit(DB rename 不可逆,需备份;或在 rename 前完整备份 auth.db)。


9. 工作量(一次发版)

  • 模块 1(身份):~2 天
  • 模块 2(rename):~3 天(全局 grep + DB 迁移)
  • 模块 3(scope/per-client):~3 天(表 + 注入逻辑 + UI)
  • 模块 4(/userinfo/token 规范):~1 天
  • 模块 5(INTEGRATION.md):~0.5 天
  • 模块 6(下游联调):~1-2 天
  • 合计 ~10-11 天(2 周),一次发版。

10. 下游接入新规范(契约,写进 INTEGRATION.md)

  1. 注册 OAuth client(client_id/secret/redirect_uri/scopes)。
  2. 声明用哪些 scope(auth 配置 client_scopes)——「我需要哪些字段」的契约。
  3. sub 做用户主键(标准,跨组织稳定)—— 绝不用 org_id 当用户身份。
  4. org_id 只用于团队/企业隔离(若需要)—— 与 sub 正交。
  5. 验 token 校验 aud(= 自己 client_id)—— 防 token 跨业务误用。
  6. 业务自定义 scope 走申请制(向 SaaS 申请,可审计)。