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 错乱。