多租户
youlai-boot-tenant(Java/Spring Boot)的多租户设计方案,基于共享数据库 + 共享 Schema 模式实现安全、可控、可扩展的多租户能力。
多租户模式
| 模式 | 隔离级别 | 成本 | 适用场景 |
|---|---|---|---|
| 独立数据库 | 最高 | 最高 | 金融、政务、大客户 |
| 共享 DB + 独立 Schema | 中等 | 中等 | 中大型 SaaS |
| 共享 DB + 共享 Schema | 基础 | 最低 | 绝大多数 SaaS |
本系统采用:共享数据库 + 共享 Schema + tenant_id 行级隔离。
优势:
- 成本最低、运维最简单
- 通过 MyBatis-Plus 多租户插件在 SQL 层统一处理
- 适合绝大多数 SaaS 场景
用户类型
系统中存在两类用户:
| 用户类型 | tenant_id | 可切换租户 | 数据访问范围 |
|---|---|---|---|
| 平台超级管理员 | 0 | ✅ | 平台数据 + 所有租户数据 |
| 平台普通用户 | 0 | ❌ | 仅平台数据 |
| 租户用户 | >0 | ❌ | 仅当前租户数据 |
典型职责:
平台管理员:
- 系统配置管理:菜单、权限、系统参数
- 租户业务管理:查看各租户数据,协助排查问题
- 业务运行监控:实时了解各租户业务运行状况
平台租户价值:
设计原则
核心原则:
tenant_id只用于数据隔离,不得用于身份判断tenant_scope用于区分用户身份类型(PLATFORM / TENANT)role用于权限控制,与租户身份解耦- 跨租户操作必须通过显式切换
tenant_id完成 tenant_id不允许为空,不存在ALL / null / *语义
设计总原则:
tenant_id管数据,tenant_scope管身份,role管权限。
权限模型
多租户的权限设计通过"三层过滤"完成:套餐 → 租户 → 角色。
用户最终可见菜单 = 套餐边界 ∩ 租户配置 ∩ 角色权限
说明:套餐决定功能上限,租户决定实际启用范围,角色决定用户可见权限。
数据模型
用户表扩展
sql
ALTER TABLE `sys_user`
ADD COLUMN `tenant_scope` varchar(20) NOT NULL DEFAULT 'TENANT'
COMMENT '租户身份标识(PLATFORM/TENANT)' AFTER `tenant_id`;用户身份定义:
- 平台用户:
tenant_id = 0,tenant_scope = PLATFORM - 租户用户:
tenant_id = 实际租户 ID,tenant_scope = TENANT
菜单与套餐表
sql
-- 菜单表:新增 scope 字段区分平台/业务菜单
CREATE TABLE `sys_menu` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL COMMENT '菜单名称',
`scope` tinyint(1) NOT NULL DEFAULT 2 COMMENT '菜单范围(1=平台菜单 2=业务菜单)',
PRIMARY KEY (`id`)
);
-- 租户套餐表:定义不同服务级别
CREATE TABLE `sys_tenant_plan` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '套餐ID',
`name` varchar(100) NOT NULL COMMENT '套餐名称',
`code` varchar(50) NOT NULL COMMENT '套餐编码',
PRIMARY KEY (`id`)
);
-- 套餐菜单关联表:定义套餐菜单边界
CREATE TABLE `sys_tenant_plan_menu` (
`plan_id` bigint NOT NULL COMMENT '套餐ID',
`menu_id` bigint NOT NULL COMMENT '菜单ID',
PRIMARY KEY (`plan_id`, `menu_id`)
);
-- 租户菜单关联表:租户个性化菜单配置
CREATE TABLE `sys_tenant_menu` (
`tenant_id` bigint NOT NULL COMMENT '租户ID',
`menu_id` bigint NOT NULL COMMENT '菜单ID',
PRIMARY KEY (`tenant_id`, `menu_id`)
);架构设计
请求级租户上下文
核心组件:
| 组件 | 说明 |
|---|---|
TenantContextFilter | 解析租户信息并写入上下文 |
TenantContextHolder | 保存当前请求的 tenant_id |
TenantLineHandler | 在 SQL 层自动追加租户条件 |
MyMetaObjectHandler | 自动填充新增数据的 tenant_id |
域名与租户映射
系统支持通过域名解析租户上下文:
vue.youlai.tech→ 平台租户(tenant_id = 0)demo.youlai.tech→ 演示租户(tenant_id = 1)
域名与租户 ID 的映射关系由 sys_tenant.domain 维护。
租户切换规则
切换条件:
- 仅
tenant_scope = PLATFORM且具备sys:tenant:switch权限的用户允许切换租户 - 切换租户前必须完成身份认证
- 目标租户必须存在且处于启用状态
切换本质:变更当前请求上下文中的 tenant_id,不改变用户归属。
配置指南
前端配置
在前端 .env 文件中开启多租户:
bash
# .env.development / .env.production
VITE_APP_TENANT_ENABLED=true租户配置步骤
创建租户套餐:
进入 平台管理 → 租户套餐,新增套餐并保存。
分配套餐菜单:
在套餐列表中点击 分配菜单,勾选应包含的功能菜单。
创建租户并绑定套餐:
进入 平台管理 → 租户管理,填写租户信息并配置域名(如 demo.youlai.tech)。
可选操作:
更换套餐或为租户微调菜单。
Nginx 配置
nginx
server {
listen 80;
# 泛域名 *.youlai.tech 会匹配所有 youlai.tech 的子域名
server_name vue.youlai.tech demo.youlai.tech *.youlai.tech;
location /prod-api/ {
proxy_set_header Host $host; # 关键:将原始请求的域名透传给后端
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://YOUR_BACKEND_API_ADDRESS/;
}
}警告:如果 Nginx 未正确配置
proxy_set_header Host $host;,后端将无法通过域名识别租户身份,导致数据隔离失效。
在线演示
演示地址:
- 平台租户视角:https://vue.youlai.tech
- 业务租户视角:https://demo.youlai.tech
测试账号:admin / 123456
平台管理员验证步骤:
- 登录平台租户,确认左侧包含 租户管理、系统配置 等平台菜单
- 使用右上角租户切换入口切换到 演示租户
- 进入 系统管理 → 用户管理,确认只看到当前租户数据
业务租户验证步骤:
- 登录业务租户,确认无租户切换入口且不包含平台菜单
- 进入 系统管理 → 用户管理,仅看到本租户用户
源码:
实现清单
以下清单用于验证实现是否符合设计预期:
tenant_id未参与身份判断tenant_scope用于区分平台用户与租户用户- 所有业务表均强制包含
tenant_id - 平台用户跨租户操作必须显式切换上下文
- 租户切换额外校验
sys:tenant:switch权限 - 不绕过多租户插件进行数据访问
相关文件
| 文件 | 说明 |
|---|---|
core/context/TenantContextHolder.java | 租户上下文持有者 |
core/filter/TenantContextFilter.java | 租户上下文过滤器 |
core/handler/TenantLineHandler.java | SQL 租户条件处理器 |
core/handler/MyMetaObjectHandler.java | 自动填充 tenant_id |
system/entity/SysTenant.java | 租户实体 |
system/entity/SysTenantPlan.java | 租户套餐实体 |
