Skip to content

多租户

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 = 0tenant_scope = PLATFORM
  • 租户用户:tenant_id = 实际租户 IDtenant_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;,后端将无法通过域名识别租户身份,导致数据隔离失效。

在线演示

演示地址

测试账号admin / 123456

平台管理员验证步骤

  1. 登录平台租户,确认左侧包含 租户管理系统配置 等平台菜单
  2. 使用右上角租户切换入口切换到 演示租户
  3. 进入 系统管理用户管理,确认只看到当前租户数据

业务租户验证步骤

  1. 登录业务租户,确认无租户切换入口且不包含平台菜单
  2. 进入 系统管理用户管理,仅看到本租户用户

源码

实现清单

以下清单用于验证实现是否符合设计预期:

  • tenant_id 未参与身份判断
  • tenant_scope 用于区分平台用户与租户用户
  • 所有业务表均强制包含 tenant_id
  • 平台用户跨租户操作必须显式切换上下文
  • 租户切换额外校验 sys:tenant:switch 权限
  • 不绕过多租户插件进行数据访问

相关文件

文件说明
core/context/TenantContextHolder.java租户上下文持有者
core/filter/TenantContextFilter.java租户上下文过滤器
core/handler/TenantLineHandler.javaSQL 租户条件处理器
core/handler/MyMetaObjectHandler.java自动填充 tenant_id
system/entity/SysTenant.java租户实体
system/entity/SysTenantPlan.java租户套餐实体

基于 MIT 许可发布