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

多租户地基(阶段 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_iddb.rs:61,含迁移 db.rs:688-704值 = username(每用户一租户)
JWT Claims.tenant_idauth.rs:24登录时从 user 带出
TokenInfo.tenant_idmodels.rs:119同上
登录 tenant_id = COALESCE(tenant_id, username)handlers.rs:850兜底为 username
超级权限 admin:manageSUPER_PERMISSIONmodels.rsmiddleware.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_idUPDATE 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:manageSUPER_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:688pragma_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.rslist_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.rsget_user_permissions / get_user_menu_treeuser→role→menu 链,roles 加 tenant_id 后天然隔离,SQL 无需改;但 assign_role 要校验 role.tenant_id == user.tenant_id
api_keys.rs / oauth_client.rsCRUD 加 tenant

6.4 模型层(models.rs

Role / ApiKey / OAuthClient / Department / AuditLogpub 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. 换 PostgreSQLsqlx 兼容迁移 + RLS 硬兜底 + 读写分离 + 数据搬迁横扩就绪
3. 租户管理 + 自助tenant CRUD / 配额 / 品牌 / 登录页配置;admin-ui 多租户化可对外卖
4. 横扩 + 可观测多实例 + nginx LB + Prometheus / OpenTelemetry / tracing生产级 SLO
5. 商业化计费 / 用量统计 / 合规导出 / SSO 企业版商业闭环

附:阶段 1 已确认的决策

  1. users.tenant_id 全部折叠到 "default" 单租户(不保留「每用户一租户」)
  2. ✅ 平台管理员复用现有 admin:manage 超级权限(= 跨租户),不新增概念
  3. menus / dict_* 平台共享,不加 tenant_id
  4. user_roles / role_menus 隐含隔离(经 role→tenant 链),不加列