scaffold-tenant -- 多租户模块

模块概述

scaffold-tenant 是基于 MyBatis-Plus 拦截器的多租户数据隔离模块。通过在 SQL 执行前自动追加 tenant_id 过滤条件,实现租户级别的数据隔离,业务代码无需关心租户字段。模块通过 ThreadLocal 管理当前请求的租户 ID,结合 TenantInterceptor 对 SELECT/UPDATE/DELETE 自动追加 WHERE 条件,并支持通过 @IgnoreTenant 注解跳过特定查询的租户过滤。

功能列表

  • 自动 SQL 追加 tenant_id 过滤条件(SELECT/UPDATE/DELETE)
  • ThreadLocal 管理当前租户 ID(TenantContext
  • @IgnoreTenant 注解跳过租户过滤
  • 可配置租户 ID 字段名
  • 可通过配置开关完全禁用

租户隔离原理

请求进入 -> JwtAuthenticationFilter 解析 Token 获取 tenantId
        -> 业务代码设置 TenantContext.setTenantId(tenantId)
        -> MyBatis 执行 SQL
        -> TenantInterceptor.beforeQuery() 自动修改 SQL:
           SELECT * FROM orders WHERE status = 1
           变为:
           SELECT * FROM orders WHERE status = 1 AND tenant_id = 1
        -> 请求结束 -> TenantContext.clear()

拦截器处理逻辑:

SQL 类型 处理方式
SELECT 在 WHERE 子句追加 AND tenant_id = ?
UPDATE 在 WHERE 子句追加 AND tenant_id = ?
DELETE 在 WHERE 子句追加 AND tenant_id = ?
INSERT (需业务层自行填充 tenant_id 字段)

配置参数

application.yml 中以 scaffold.tenant 为前缀配置:

scaffold:
  tenant:
    enabled: true               # 是否启用多租户
    tenant-id-column: "tenant_id"  # 租户 ID 数据库列名
参数 类型 默认值 说明
enabled boolean true 是否启用多租户功能
tenant-id-column String "tenant_id" 数据库中租户 ID 的列名

TenantContext API

TenantContext 是基于 ThreadLocal 的租户上下文管理器,用于存储当前请求的租户 ID。

// 设置当前租户 ID(通常在 Filter/Interceptor 中调用)
TenantContext.setTenantId(1L);

// 获取当前租户 ID(可在 Service/Mapper 层调用)
Long tenantId = TenantContext.getTenantId();

// 清除当前租户 ID(请求结束时调用,避免内存泄漏)
TenantContext.clear();

// 租户 ID 字段名常量
String column = TenantContext.TENANT_ID_COLUMN; // "tenant_id"
方法 说明
setTenantId(Long tenantId) 设置当前线程的租户 ID
getTenantId() 获取当前线程的租户 ID,未设置返回 null
clear() 清除当前线程的租户 ID,防止内存泄漏

@IgnoreTenant 使用

当某些查询需要跨租户访问数据时(如系统表、共享配置、管理员查询全部数据),可使用 @IgnoreTenant 注解跳过租户过滤。

标注在 Mapper 方法上

@Mapper
public interface UserMapper extends BaseMapper<User> {

    /**
     * 根据用户名查询(跨租户,用于登录校验)
     */
    @IgnoreTenant
    User selectByUsername(@Param("username") String username);

    /**
     * 查询所有租户的用户数量(管理后台统计)
     */
    @IgnoreTenant
    Long selectAllUserCount();
}

标注在 Repository 类上

@IgnoreTenant  // 整个 Repository 的所有方法均跳过租户过滤
public class SystemConfigRepository extends ServiceImpl<SystemConfigMapper, SystemConfig> {
}

标注在 Service 方法上

@Service
@RequiredArgsConstructor
public class TenantManagementService {

    private final TenantMapper tenantMapper;

    /**
     * 查询所有租户列表(超级管理员功能)
     */
    @IgnoreTenant
    public List<Tenant> listAllTenants() {
        return tenantMapper.selectList(null);
    }
}

集成方式

1. 添加 Maven 依赖

<dependency>
    <groupId>com.scaffold</groupId>
    <artifactId>scaffold-tenant</artifactId>
</dependency>

2. 确保数据库表包含 tenant_id 字段

所有需要租户隔离的业务表均需包含 tenant_id 字段:

CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    tenant_id BIGINT NOT NULL DEFAULT 1 COMMENT '租户ID',
    -- 其他业务字段
    deleted INT DEFAULT 0,
    INDEX idx_tenant_id (tenant_id)
);

3. 实体类无需额外注解

继承 BaseEntity 的实体类无需添加租户相关注解,拦截器自动处理。如需在实体中引用 tenantId 字段,正常定义即可:

public class Order extends BaseEntity {
    private Long tenantId;  // 与数据库列 tenant_id 对应
    private String orderNo;
}

4. 在认证流程中设置租户上下文

通常在 Security 过滤器或自定义拦截器中,从 JWT Token 解析出租户 ID 并设置到上下文:

@Component
public class TenantContextFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        try {
            LoginUser loginUser = SecurityContextHolderUtils.getCurrentUser();
            if (loginUser != null && loginUser.getTenantId() != null) {
                TenantContext.setTenantId(loginUser.getTenantId());
            }
            chain.doFilter(request, response);
        } finally {
            TenantContext.clear();
        }
    }
}

5. INSERT 场景需手动填充

TenantInterceptor 当前主要处理 SELECT/UPDATE/DELETE 的 WHERE 追加。INSERT 场景下 tenant_id 需要业务层手动赋值:

@Service
@RequiredArgsConstructor
public class OrderService {

    public void createOrder(OrderCreateRequest request) {
        Order order = BeanCopyUtils.copy(request, Order.class);
        order.setTenantId(TenantContext.getTenantId());  // 手动设置租户 ID
        orderRepository.save(order);
    }
}

自动配置说明

本模块通过 TenantAutoConfiguration 注册:

Bean 条件 说明
TenantInterceptor scaffold.tenant.enabled=true 且存在 MybatisPlusInterceptor 租户拦截器,添加到 MyBatis-Plus 拦截器链

自动配置通过 @ConditionalOnProperty(prefix = "scaffold.tenant", name = "enabled", havingValue = "true", matchIfMissing = true) 控制,默认启用。

注意事项

  • TenantContext 基于 ThreadLocal,在异步线程场景下租户 ID 不会自动传递,需要手动处理。
  • 使用 @IgnoreTenant 注解时需谨慎,确保不会泄露其他租户的敏感数据。
  • 拦截器通过 JSQLParser 解析和修改 SQL,对于极端复杂的 SQL(如嵌套子查询、UNION)可能存在兼容性问题,此时可使用 @IgnoreTenant 跳过并手动编写带租户条件的 SQL。
  • 建议所有需要租户隔离的表建立 tenant_id 索引,避免全表扫描。
  • TenantContext.clear() 必须在请求结束时调用(通常在 Filter 的 finally 块中),否则会导致线程复用时租户 ID 错乱。