多租户地基(阶段 1)实施方案
auth(OIDC + RBAC,Rust/Axum + SQLite/Redis)产品化为多租户 IDaaS SaaS 的第 1 阶段。 目标规模:中等(多租户 SaaS,万级用户)。整体路线见文末「后续阶段」。 本文档为可追溯的设计与实施依据,定稿于 2026-07-08。
1. 阶段 1 目标与边界
目标:在单库(SQLite)上跑通「一个租户上下文贯穿所有 handler」——所有业务数据按租户隔离,平台管理员可跨租户。最小可验证里程碑。
不做(明确边界):
- ❌ 不换 PostgreSQL(阶段 2)
- ❌ 不做租户管理 UI / 自助 / 配额计费(阶段 3 / 5)
- ❌ 不做
menus/dict_*多租户(平台共享) - ❌ 不做 RLS(SQLite 没有,靠代码 + 跨租户测试兜底;阶段 2 上 PostgreSQL 再加 RLS 做硬兜底)
- ❌ 前端最小化(阶段 3 做租户切换器)
成功标准:升级零破坏(现有数据归 default 租户,功能不变)+ 跨租户隔离测试通过。
2. 现状摸底
auth 早就有 tenant_id 雏形,但语义是「每用户一个独立租户」,不是真正的多用户共享租户:
| 已有 | 位置 | 现状 |
|---|---|---|
users.tenant_id 列 | db.rs:61,含迁移 db.rs:688-704 | 值 = username(每用户一租户) |
JWT Claims.tenant_id | auth.rs:24 | 登录时从 user 带出 |
TokenInfo.tenant_id | models.rs:119 | 同上 |
登录 tenant_id = COALESCE(tenant_id, username) | handlers.rs:850 | 兜底为 username |
超级权限 admin:manage(SUPER_PERMISSION) | models.rs、middleware.rs:105 | 绕过所有权限检查 |
缺失:
- ❌ 无
tenants主表(租户注册/状态/配额) - ❌
roles/api_keys/audit_logs/departments/oauth_clients都没有tenant_id - ❌
menus/dict_type/dict_data未定性(平台共享 or 租户独立) - ❌ 无
TenantContext中间件(tenant_id在 Claims 里但没用于数据隔离)
阶段 1 = 改造现有雏形语义 + 补齐缺失。
3. 设计决策
3.1 哪些资源按租户隔离
| 资源 | 加 tenant_id? | 理由 |
|---|---|---|
users | ✓(改语义) | 已有列,值从 “=username” 改为 “=租户标识” |
roles departments api_keys audit_logs oauth_clients | ✓ 新增 | 每租户独立 |
user_roles role_menus | 隐含 | 经 user→tenant、role→tenant 链隔离,不重复加列 |
menus dict_type dict_data | ✗ 平台共享 | 权限/字典是平台级模板,所有租户共用 |
3.2 tenant_id 类型与值
- 类型:
VARCHAR(50)(与现有users.tenant_id一致) - 值:可读标识,如
"default"、"acme"、"xyz" - 主表:
tenants(id, name, status, max_users, ...)
3.3 default 租户
现有全部数据归入 "default" 租户,保证升级零破坏:
users.tenant_id:UPDATE users SET tenant_id='default' WHERE tenant_id IS NULL OR tenant_id=username- 其他业务表:
ALTER ADD COLUMN tenant_id+UPDATE ... SET tenant_id='default' tenants表插入('default', '默认租户')
4. 平台 admin vs 租户 admin
多租户系统两层管理员,权限范围完全不同:
平台运营方(我们)
└─ 平台管理员(platform admin) ← 看所有租户、建/冻结租户、全局审计
↓ tenant_id 隔离
租户 acme
└─ 租户管理员(acme 的 IT) ← 只管 acme,看不到 xyz
↓
租户 xyz
└─ 租户管理员(xyz 的 IT) ← 只管 xyz
复用 admin:manage 作为「平台管理员」标识
auth 已有超级权限 admin:manage(SUPER_PERMISSION):持有人绕过所有权限检查(middleware.rs:105)。现 admin 用户即持有它。
多租户后,这个超级权限的自然延伸就是「平台管理员」:
| 谁 | 持 admin:manage? | 中间件算出的 tenant 作用域 | list_users 看到 |
|---|---|---|---|
admin(平台运营) | ✓ | None(不过滤) | 所有租户的用户 |
acme_admin(acme 的 IT) | ✗ | Some("acme") | 只有 acme 的用户 |
xyz_user(xyz 普通员工) | ✗ | Some("xyz") | 只有 xyz 的用户(且受 RBAC 约束) |
一句话:
admin:manage= 平台管理员 = 不受租户隔离 = 跨租户看全部;没有它就是租户用户 = 被自己的 tenant_id 锁住。
后续(阶段 3)若要更清晰区分「平台运营」与「租户管理员」,可新增 platform:admin 权限,但阶段 1 复用 admin:manage 即可,避免引入新概念。
5. 数据模型变更(src/db.rs)
5.1 新建 tenants 主表
CREATE TABLE IF NOT EXISTS tenants (
id VARCHAR(50) PRIMARY KEY, -- 'default', 'acme'
name VARCHAR(100) NOT NULL,
status CHAR(1) DEFAULT '0', -- 0=正常 1=冻结
max_users INTEGER DEFAULT 0, -- 配额(0=不限,阶段 5 启用)
created_at DATETIME DEFAULT (datetime('now','+8 hours')),
updated_at DATETIME DEFAULT (datetime('now','+8 hours'))
);
INSERT OR IGNORE INTO tenants (id, name) VALUES ('default', '默认租户');
5.2 业务表加 tenant_id(增量迁移)
通用模式(参考 db.rs:688 的 pragma_table_info 检测),对 roles / api_keys / audit_logs / departments / oauth_clients 各执行:
#![allow(unused)]
fn main() {
let has: bool = sqlx::query_scalar::<_, i64>(
&format!("SELECT COUNT(*) FROM pragma_table_info('{tbl}') WHERE name='tenant_id'")
).fetch_one(pool).await? > 0;
if !has {
sqlx::query(&format!("ALTER TABLE {tbl} ADD COLUMN tenant_id VARCHAR(50)")).execute(pool).await?;
sqlx::query(&format!("UPDATE {tbl} SET tenant_id='default' WHERE tenant_id IS NULL")).execute(pool).await?;
sqlx::query(&format!("CREATE INDEX IF NOT EXISTS idx_{tbl}_tenant ON {tbl}(tenant_id)")).execute(pool).await?;
}
}
5.3 users.tenant_id 语义迁移(关键)
UPDATE users SET tenant_id='default' WHERE tenant_id IS NULL OR tenant_id=username;
把「每用户一租户」折叠回 default。实施前必须 grep 全部 tenant_id 用法逐一 review(重点 handlers.rs:850 的 COALESCE、OIDC claim 里的 tenant_id)。
6. 后端改造
6.1 TenantContext(src/middleware.rs)
auth_middleware 验完 token 拿到 Claims 后,算 tenant 作用域注入 RequestMeta:
#![allow(unused)]
fn main() {
pub struct RequestMeta {
pub ip: Option<String>,
pub user_agent: Option<String>,
pub tenant_id: Option<String>, // None = 平台 admin(跨租户);Some(t) = 租户作用域
pub is_platform_admin: bool,
}
// auth_middleware 内:
let is_admin = claims.permissions.contains(&SUPER_PERMISSION.to_string());
let meta = RequestMeta {
ip, user_agent,
tenant_id: if is_admin { None } else { claims.tenant_id.clone() },
is_platform_admin: is_admin,
};
}
6.2 tenant_scope helper
#![allow(unused)]
fn main() {
/// 返回 WHERE 片段与 bind 值:None = 平台 admin 不过滤;Some = 租户作用域
fn tenant_scope(meta: &RequestMeta) -> Option<&str> {
meta.tenant_id.as_deref() // Some(t) → 加 "tenant_id = ?"; None → 不加
}
}
list/get 类查询按是否 Some 拼接 WHERE tenant_id = ?;INSERT 一律 bind tenant_id(平台 admin 创建资源时显式指定目标租户)。
6.3 改造清单(实施时 grep 定位)
grep -rn "FROM users\|FROM roles\|FROM api_keys\|FROM audit_logs\|FROM departments\|FROM oauth_clients" src/
| 文件 | 改造点 |
|---|---|
admin.rs | list_users / create_user / list_roles / create_role / api_keys CRUD / audit / departments / oauth_clients 全部加 tenant_scope + INSERT 带 tenant_id |
handlers.rs | 用户注册(已有 tenant_id 逻辑,改语义)、userinfo(OIDC claim tenant_id) |
rbac.rs | get_user_permissions / get_user_menu_tree 经 user→role→menu 链,roles 加 tenant_id 后天然隔离,SQL 无需改;但 assign_role 要校验 role.tenant_id == user.tenant_id |
api_keys.rs / oauth_client.rs | CRUD 加 tenant |
6.4 模型层(models.rs)
Role / ApiKey / OAuthClient / Department / AuditLog 加 pub tenant_id: String,与 SQL 对齐。
7. 前端(阶段 1 不做)
阶段 1 后端为主。前端最小化(现有 admin-ui 仍按平台 admin 用,看全部)。租户切换器、租户管理 UI 在阶段 3。
8. 验证(SQLite 无 RLS,靠测试兜底)
- 跨租户隔离:建 tenantA / tenantB + 各自用户/角色/apikey,登录 A 调 list 接口,断言看不到 B 的数据。
- 平台 admin 跨租户:admin 登录 list,看到所有租户数据。
- 升级兼容:default 租户用户登录、菜单、权限、OIDC 全部正常。
建议把跨租户隔离测试做成集成测试常驻,防止后续 handler 漏改。
9. 风险与回滚
| 风险 | 应对 |
|---|---|
users.tenant_id 语义从 username→default,影响 handlers.rs COALESCE 与 OIDC claim | 实施前 grep 全部 tenant_id 用法逐一 review;claim 里 tenant_id 改为 =default |
| handler 漏加 tenant_scope → 数据泄露 | 强制改造清单 + 跨租户集成测试;阶段 2 上 PostgreSQL RLS 硬兜底 |
| RBAC 链跨租户错配(user 拿到别租户 role) | assign_role 校验 role.tenant_id == user.tenant_id |
| 回滚 | 迁移只 ADD COLUMN + UPDATE,不动旧列;回滚 = 还原代码(列留着无害) |
10. 工作量
- db.rs 建表 + 5 表迁移 + users 语义迁移:~0.5 天
- 后端 handler 逐个加 tenant_scope(grep + 改):~2 天
- 模型 + helper + 中间件:~0.5 天
- 跨租户隔离测试:~1 天
- 合计 ~1 周(不含前端)
11. 后续阶段路线(追溯)
| 阶段 | 内容 | 产出 |
|---|---|---|
| 1. 多租户地基(本文档) | tenant_id + TenantContext + 业务表隔离,SQLite 先跑通 | 单库多租户 |
| 2. 换 PostgreSQL | sqlx 兼容迁移 + RLS 硬兜底 + 读写分离 + 数据搬迁 | 横扩就绪 |
| 3. 租户管理 + 自助 | tenant CRUD / 配额 / 品牌 / 登录页配置;admin-ui 多租户化 | 可对外卖 |
| 4. 横扩 + 可观测 | 多实例 + nginx LB + Prometheus / OpenTelemetry / tracing | 生产级 SLO |
| 5. 商业化 | 计费 / 用量统计 / 合规导出 / SSO 企业版 | 商业闭环 |
附:阶段 1 已确认的决策
- ✅
users.tenant_id全部折叠到"default"单租户(不保留「每用户一租户」) - ✅ 平台管理员复用现有
admin:manage超级权限(= 跨租户),不新增概念 - ✅
menus/dict_*平台共享,不加 tenant_id - ✅
user_roles/role_menus隐含隔离(经 role→tenant 链),不加列