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 Actions、Keycloak client scopes、Azure optional claims |
| per-client 配置 | 每个 client 配自己的 scope/claim(Keycloak client scopes / Auth0 Actions 按 client_id / Azure app registration) | scope vs roles |
| 多租户 | 单部署多 Organization(逻辑隔离),趋势替代每租户独立 realm | Auth0 Organizations、Keycloak 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 配置)
| scope | claim | 谁该要 |
|---|---|---|
openid(默认) | sub | 所有下游 |
profile | preferred_username, name, picture | 要展示用户名的 |
email | email, email_verified | 要邮箱的 |
auth:permissions | permissions[] | 要做 RBAC 授权的 |
auth:org | org_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_id、roles.org_id、api_keys.org_id、audit_logs.org_id、departments.org_id、oauth_clients.org_id、tenant_oidc_providers→org_idp_providers(org 级 IdP)。tenants表 →organizations(org 主表)。- 代码:
tenant_id→org_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.rsClaims: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列 renameorg_id(SQLite 不支持直接 rename column,用「新建列 + 迁数据 + 旧列留空兼容」或重建表;或若可接受,保留列名 tenant_id 但代码层全用 org_id——决策:彻底 rename,重建表迁移)。 tenants→organizations表 rename。- 代码全局
tenant_id→org_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.rs | Claims(sub/aud/preferred_username) + 签 token |
| src/models.rs | Claims/User/Role/… tenant_id→org_id + ClientScope/OAuthClientScope |
| src/middleware.rs | RequestMeta tenant_id→org_id |
| src/db.rs | 表列 rename org_id + organizations + client_scopes + oauth_client_scopes + seed |
| src/admin.rs | tenant→org + scope CRUD + client scope 配置 |
| src/handlers.rs | tenant→org |
| src/sso.rs | tenant→org |
| src/tenant.rs → src/org.rs | rename + tenant→org |
| src/oauth.rs | aud + scope 注入 claim |
| src/oauth_client.rs | tenant→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)
- 注册 OAuth client(client_id/secret/redirect_uri/scopes)。
- 声明用哪些 scope(auth 配置 client_scopes)——「我需要哪些字段」的契约。
- 用
sub做用户主键(标准,跨组织稳定)—— 绝不用 org_id 当用户身份。 - org_id 只用于团队/企业隔离(若需要)—— 与 sub 正交。
- 验 token 校验 aud(= 自己 client_id)—— 防 token 跨业务误用。
- 业务自定义 scope 走申请制(向 SaaS 申请,可审计)。