Files
FastapiAdmin/backend/docs/多租户数据隔离设计方案.md
T
zhangtao 9a39a64faf feat(backend): 实现多租户数据隔离与权限管理架构
refactor(backend): 重构模型基类支持租户与客户隔离
feat(backend): 添加客户模块相关模型、CRUD和参数校验
docs(backend): 新增SaaS数据隔离设计方案文档
refactor(backend): 优化日志模块并添加类型注解
fix(backend): 修正字典模块查询参数移除creator字段

style(frontend): 统一按钮组件代码格式
fix(frontend): 修复表格序号计算逻辑
chore(frontend): 更新lint脚本使用pnpm替代npm
2025-11-23 18:59:43 +08:00

12 KiB

SaaS多租户数据隔离设计方案

📋 目录

  1. 设计概述
  2. 数据隔离层级
  3. 表级别隔离规则
  4. 数据权限实现
  5. SQL查询示例
  6. 最佳实践

设计概述

本系统采用三层数据隔离架构:

系统层 (System Level)
  └── 租户层 (Tenant Level)
        ├── 部门/角色/用户等组织架构
        └── 客户层 (Customer Level)
              └── 客户用户及业务数据

核心隔离字段

字段 用途 必填性
tenant_id 租户隔离,实现SaaS多租户架构 大部分表必填
customer_id 客户隔离,租户下的二级隔离 部分表可选
created_id 记录创建者,用于数据权限控制 可选
updated_id 记录更新者,用于审计追踪 可选

数据隔离层级

1. 系统级 (tenant_id=1 或 NULL)

适用场景: 平台管理、系统配置

特征:

  • 由平台超级管理员管理
  • 所有租户共享或可见
  • 如: 系统菜单、系统字典、系统参数

示例表:

  • system_tenant (租户表本身)
  • system_menu (tenant_id=NULL的系统菜单)
  • system_dict_type (tenant_id=NULL的系统字典)

2. 租户级 (tenant_id>1, customer_id=NULL)

适用场景: 租户内部管理

特征:

  • 属于特定租户,租户间完全隔离
  • 租户管理员可管理
  • 如: 部门、角色、岗位、租户用户

示例表:

  • system_dept - 部门
  • system_role - 角色
  • system_position - 岗位
  • system_user (租户用户)

3. 客户级 (tenant_id>1, customer_id>1)

适用场景: 租户下的客户业务数据

特征:

  • 属于租户下的特定客户
  • 客户用户只能访问本客户数据
  • 如: 客户用户、客户专属通知、客户业务数据

示例表:

  • system_user (客户用户: customer_id>1)
  • system_notice (客户通知: customer_id>1)
  • system_log (客户用户操作日志)

表级别隔离规则

完整隔离规则表

表名 tenant_id customer_id 说明
TenantModel 租户表本身,不需要隔离字段
CustomerModel 必填 客户属于租户,不属于客户
UserModel 必填 可选 租户用户(NULL)或客户用户(>1)
DeptModel 必填 部门是租户级组织架构
RoleModel 必填 角色是租户级权限管理
PositionModel 必填 岗位是租户级职位定义
MenuModel 可选 NULL=系统菜单,>1=租户菜单
DictTypeModel 可选 NULL=系统字典,>1=租户字典
DictDataModel 可选 继承字典类型的隔离级别
NoticeModel 必填 可选 NULL=租户通知,>1=客户通知
OperationLogModel 必填 可选 记录操作人的租户和客户信息
ParamsModel 可选 NULL=系统参数,>1=租户参数

业务表隔离规则 (示例)

对于自定义业务表,根据业务场景选择:

# 场景1: 租户级业务表 (所有租户用户共享)
class ProductModel(ModelMixin):
    tenant_id: Mapped[int]  # 必填
    customer_id: Mapped[int | None] = None  # 不需要
    
# 场景2: 客户级业务表 (客户用户独享)
class OrderModel(ModelMixin):
    tenant_id: Mapped[int]  # 必填
    customer_id: Mapped[int | None]  # 可选,客户订单时必填

数据权限实现

角色数据权限 (data_scope字段)

RoleModel.data_scope 定义了角色的数据访问范围:

名称 SQL过滤条件 使用场景
1 仅本人数据权限 WHERE created_id = current_user.id 普通员工只能看自己创建的数据
2 本部门数据权限 WHERE user.dept_id = current_user.dept_id 部门经理看本部门所有人的数据
3 本部门及以下数据权限 WHERE dept.tree_path LIKE 'current_dept.tree_path%' 总监看本部门及所有下级部门数据
4 全部数据权限 WHERE tenant_id = current_user.tenant_id 租户管理员看租户内所有数据
5 自定义数据权限 WHERE dept_id IN (role关联的部门列表) 跨部门权限,如人事看多个指定部门

权限叠加规则

一个用户有多个角色时:

  1. 取所有角色 data_scope最大值 (4>3>2>5>1)
  2. 当包含 data_scope=5 时,合并所有角色关联的部门

客户用户特殊限制:

  • 无论 data_scope 如何,都必须额外加上:
    AND customer_id = current_user.customer_id
    
  • 即客户用户永远只能看本客户的数据

SQL查询示例

1. 仅本人数据权限 (data_scope=1)

# 租户用户查询
query = (
    select(OrderModel)
    .where(OrderModel.tenant_id == current_user.tenant_id)
    .where(OrderModel.created_id == current_user.id)
)

# 客户用户查询 (额外限制客户)
query = (
    select(OrderModel)
    .where(OrderModel.tenant_id == current_user.tenant_id)
    .where(OrderModel.customer_id == current_user.customer_id)
    .where(OrderModel.created_id == current_user.id)
)

2. 本部门数据权限 (data_scope=2)

# 查询本部门所有用户创建的数据
dept_user_ids = (
    select(UserModel.id)
    .where(UserModel.dept_id == current_user.dept_id)
)

query = (
    select(OrderModel)
    .where(OrderModel.tenant_id == current_user.tenant_id)
    .where(OrderModel.created_id.in_(dept_user_ids))
)

3. 本部门及以下数据权限 (data_scope=3)

# 利用 dept.tree_path 字段高效查询
current_tree_path = current_user.dept.tree_path  # 如: /1/3/

# 查询本部门及所有子部门
sub_dept_ids = (
    select(DeptModel.id)
    .where(DeptModel.tree_path.like(f'{current_tree_path}%'))
)

# 查询这些部门的所有用户
dept_user_ids = (
    select(UserModel.id)
    .where(UserModel.dept_id.in_(sub_dept_ids))
)

query = (
    select(OrderModel)
    .where(OrderModel.tenant_id == current_user.tenant_id)
    .where(OrderModel.created_id.in_(dept_user_ids))
)

4. 全部数据权限 (data_scope=4)

# 租户管理员 - 查询租户内所有数据
query = (
    select(OrderModel)
    .where(OrderModel.tenant_id == current_user.tenant_id)
)

# 客户用户 - 即使有全部权限也只能看本客户
query = (
    select(OrderModel)
    .where(OrderModel.tenant_id == current_user.tenant_id)
    .where(OrderModel.customer_id == current_user.customer_id)
)

5. 自定义数据权限 (data_scope=5)

# 获取角色关联的部门列表
custom_dept_ids = (
    select(RoleDeptsModel.dept_id)
    .join(UserRolesModel, UserRolesModel.role_id == RoleDeptsModel.role_id)
    .where(UserRolesModel.user_id == current_user.id)
)

# 查询这些部门的用户
dept_user_ids = (
    select(UserModel.id)
    .where(UserModel.dept_id.in_(custom_dept_ids))
)

query = (
    select(OrderModel)
    .where(OrderModel.tenant_id == current_user.tenant_id)
    .where(OrderModel.created_id.in_(dept_user_ids))
)

最佳实践

1. 新建业务表时的决策树

是否需要多租户隔离?
  ├─ 是 → 添加 tenant_id (必填)
  │      └─ 是否需要客户级隔离?
  │           ├─ 是 → 添加 customer_id (可选)
  │           └─ 否 → 不添加 customer_id
  └─ 否 → 系统级表,不添加 tenant_id

2. 数据权限过滤中间件

建议在 FastAPI 依赖注入中实现数据权限过滤:

from typing import Optional
from sqlalchemy import Select

class DataPermissionFilter:
    """数据权限过滤器"""
    
    def __init__(self, current_user: UserModel):
        self.current_user = current_user
        self.max_data_scope = self._get_max_data_scope()
    
    def _get_max_data_scope(self) -> int:
        """获取用户所有角色的最大权限范围"""
        if not self.current_user.roles:
            return 1  # 默认仅本人
        return max(int(role.data_scope) for role in self.current_user.roles)
    
    def apply_filter(self, query: Select, model) -> Select:
        """应用数据权限过滤"""
        # 1. 基础租户隔离
        query = query.where(model.tenant_id == self.current_user.tenant_id)
        
        # 2. 客户用户额外隔离
        if self.current_user.customer_id:
            query = query.where(model.customer_id == self.current_user.customer_id)
        
        # 3. 数据权限过滤
        if self.max_data_scope == 1:  # 仅本人
            query = query.where(model.created_id == self.current_user.id)
        
        elif self.max_data_scope == 2:  # 本部门
            dept_user_ids = self._get_dept_user_ids(self.current_user.dept_id)
            query = query.where(model.created_id.in_(dept_user_ids))
        
        elif self.max_data_scope == 3:  # 本部门及以下
            sub_dept_user_ids = self._get_sub_dept_user_ids(self.current_user.dept)
            query = query.where(model.created_id.in_(sub_dept_user_ids))
        
        elif self.max_data_scope == 4:  # 全部数据
            pass  # 已通过租户隔离,无需额外过滤
        
        elif self.max_data_scope == 5:  # 自定义
            custom_dept_user_ids = self._get_custom_dept_user_ids()
            query = query.where(model.created_id.in_(custom_dept_user_ids))
        
        return query

3. 查询时必须检查的字段

任何业务查询都应该包含:

# ✅ 正确的查询
query = (
    select(Model)
    .where(Model.tenant_id == current_user.tenant_id)  # 必须
    # 如果用户是客户用户
    .where(Model.customer_id == current_user.customer_id)  # 必须
    # 然后再应用其他业务过滤
)

# ❌ 错误的查询 (缺少租户隔离)
query = select(Model).where(Model.name == 'xxx')  # 危险!

4. 审计字段的使用

# 创建记录时
new_record = Model(
    tenant_id=current_user.tenant_id,
    customer_id=current_user.customer_id,  # 如果是客户用户
    created_id=current_user.id,
    updated_id=current_user.id,
    # ... 其他字段
)

# 更新记录时
record.updated_id = current_user.id
record.updated_time = datetime.now()

5. 树形路径 (tree_path) 的维护

def update_dept_tree_path(dept: DeptModel, session: AsyncSession):
    """更新部门树路径"""
    if dept.parent_id:
        parent = session.get(DeptModel, dept.parent_id)
        dept.tree_path = f"{parent.tree_path}{dept.id}/"
    else:
        dept.tree_path = f"/{dept.id}/"
    
    # 递归更新所有子部门的 tree_path
    for child in dept.children:
        update_dept_tree_path(child, session)

6. 性能优化建议

索引:

-- 必须创建的索引
CREATE INDEX idx_tenant_id ON table_name(tenant_id);
CREATE INDEX idx_customer_id ON table_name(customer_id);
CREATE INDEX idx_created_id ON table_name(created_id);
CREATE INDEX idx_dept_tree_path ON system_dept(tree_path);

-- 联合索引 (提高查询性能)
CREATE INDEX idx_tenant_customer ON table_name(tenant_id, customer_id);

分区表 (当数据量巨大时):

-- 按租户分区
CREATE TABLE orders (
    id BIGINT,
    tenant_id INT,
    -- ...
) PARTITION BY HASH(tenant_id) PARTITIONS 10;

附录: 常见问题

Q1: 为什么 TenantModel 没有 tenant_id?

A: 租户表本身就是顶层隔离单位,它不属于任何租户。如果加上 tenant_id 会导致循环引用。

Q2: customer_id 何时必填?

A:

  • 用户表: 当 user_type=2 (客户用户)时必填
  • 业务表: 当该数据仅属于特定客户时必填
  • 日志/通知表: 当记录客户用户操作或发送给客户时必填

Q3: 如何处理跨租户的场景?

A: 系统设计为严格租户隔离,不支持跨租户查询。如需跨租户集成,应通过 API 接口或消息队列实现。

Q4: 客户用户能否拥有 data_scope=4 (全部数据权限)?

A: 可以,但实际查询时仍会限制为 customer_id = current_user.customer_id,即"客户内的全部数据"。

Q5: 如何实现"查看下属数据"的权限?

A: 通过部门树结构实现:

  1. 设置领导为部门负责人
  2. 分配 data_scope=3 (本部门及以下)的角色
  3. 系统自动通过 tree_path 查询所有下级部门数据

修订历史

版本 日期 说明
v1.0 2025-11-22 初始版本,完成数据隔离设计