📖 前言
在当今云计算时代,SaaS(Software as a Service) 模式已经成为企业服务的主流形态。作为SaaS系统的核心架构要素,多租户技术能够让单个应用实例为多个客户(租户)提供服务,同时确保各租户数据的安全隔离。
本文将深入探讨如何使用 MyBatis拦截器实现高效、安全的行级数据隔离方案,为正在构建或优化SaaS系统的开发者提供完整的技术解决方案。
🏗️ 多租户数据隔离方案对比
在SaaS系统中,数据隔离主要有三种实现方案:
隔离级别 | 实现方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
数据库级 | 每个租户独立数据库 | 隔离性最高、安全性最好 | 成本高、扩展性差 | 金融、政府等高安全要求 |
Schema级 | 共享数据库,独立Schema | 良好隔离性、中等成本 | 扩展性受限 | 中大型企业客户 |
行级 | 共享数据库和Schema | 成本最低、扩展性最佳 | 依赖应用层安全 | 标准化SaaS产品 |
行级数据隔离 因其成本效益 和可扩展性 成为大多数SaaS企业 的首选方案。其核心原理是在所有业务表中添加tenant_id字段,通过SQL拦截自动添加租户过滤条件。
🔧 MyBatis拦截器核心实现
环境准备
首先在pom.xml中添加必要依赖:
xml
<dependencies>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<!-- JSqlParser for SQL解析 -->
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>4.5</version>
</dependency>
</dependencies>
1. 租户上下文管理
java
@Component
public class TenantContext {
private static final ThreadLocal<Long> CURRENT_TENANT = new ThreadLocal<>();
public static void setCurrentTenant(Long tenantId) {
CURRENT_TENANT.set(tenantId);
}
public static Long getCurrentTenant() {
return CURRENT_TENANT.get();
}
public static void clear() {
CURRENT_TENANT.remove();
}
}
2. Web层租户拦截器
java
@Component
public class TenantInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) {
// 从请求头获取租户ID
String tenantIdHeader = request.getHeader("X-Tenant-ID");
if (StringUtils.isNotBlank(tenantIdHeader)) {
TenantContext.setCurrentTenant(Long.valueOf(tenantIdHeader));
} else {
throw new IllegalArgumentException("Tenant ID is required");
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
TenantContext.clear();
}
}
3. MyBatis多租户拦截器(核心代码)
java
@Component
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare",
args = {Connection.class, Integer.class})
})
public class TenantInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(TenantInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
logger.info("🚀 MyBatis拦截器开始执行...");
// 获取StatementHandler
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
// 获取MappedStatement
MappedStatement mappedStatement = (MappedStatement)
metaObject.getValue("delegate.mappedStatement");
logger.info("📝 拦截的Mapper方法: {}", mappedStatement.getId());
// 获取当前租户ID
Long tenantId = TenantContext.getCurrentTenant();
if (tenantId == null) {
logger.warn("⚠️ 未找到租户ID,跳过拦截");
return invocation.proceed();
}
// 获取并修改SQL
BoundSql boundSql = statementHandler.getBoundSql();
String originalSql = boundSql.getSql();
String modifiedSql = addTenantCondition(originalSql, tenantId);
// 设置修改后的SQL
metaObject.setValue("delegate.boundSql.sql", modifiedSql);
return invocation.proceed();
}
/**
* 核心方法:为SQL添加租户条件
*/
private String addTenantCondition(String sql, Long tenantId) {
try {
Statement statement = CCJSqlParserUtil.parse(sql);
if (statement instanceof Select) {
return processSelect((Select) statement, tenantId);
} else if (statement instanceof Update) {
return processUpdate((Update) statement, tenantId);
} else if (statement instanceof Delete) {
return processDelete((Delete) statement, tenantId);
}
} catch (JSQLParserException e) {
logger.warn("SQL解析失败,使用正则降级处理");
return addTenantConditionWithRegex(sql, tenantId);
}
return sql;
}
private String processSelect(Select select, Long tenantId) {
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
Expression where = plainSelect.getWhere();
// 创建租户过滤条件
Expression tenantCondition = new EqualsTo()
.withLeftExpression(new Column("tenant_id"))
.withRightExpression(new LongValue(tenantId));
if (where == null) {
plainSelect.setWhere(tenantCondition);
} else {
plainSelect.setWhere(new AndExpression(where, tenantCondition));
}
return select.toString();
}
// 类似的processUpdate和processDelete方法...
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
}
🎯 注解驱动增强方案
为了提供更灵活的控制,我们可以实现注解驱动的多租户方案:
1. 注解定义
java
// 跳过租户过滤注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SkipTenantFilter {
String value() default "";
}
// 强制租户过滤注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ForceTenantFilter {
String tenantIdColumn() default "tenant_id";
}
2. 注解驱动拦截器
java
@Component
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare",
args = {Connection.class, Integer.class})
})
public class AnnotationDrivenTenantInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(handler);
MappedStatement mappedStatement = (MappedStatement)
metaObject.getValue("delegate.mappedStatement");
// 解析方法注解
Method method = getMethodFromMappedStatement(mappedStatement);
if (method == null) return invocation.proceed();
// 注解决策逻辑
if (method.isAnnotationPresent(SkipTenantFilter.class)) {
return invocation.proceed(); // 跳过处理
}
Long tenantId = TenantContext.getCurrentTenant();
if (tenantId != null) {
BoundSql boundSql = handler.getBoundSql();
String modifiedSql = addTenantCondition(boundSql.getSql(), tenantId);
metaObject.setValue("delegate.boundSql.sql", modifiedSql);
}
return invocation.proceed();
}
}
3. Mapper使用示例
java
@Mapper
public interface UserMapper {
// 默认启用租户过滤
@Select("SELECT * FROM users WHERE status = #{status}")
List<User> findByStatus(@Param("status") String status);
// 跳过租户过滤(管理员使用)
@SkipTenantFilter
@Select("SELECT * FROM users")
List<User> findAllForAdmin();
// 强制指定租户字段
@ForceTenantFilter(tenantIdColumn = "company_id")
@Select("SELECT * FROM users")
List<User> findByCompany();
}
📊 执行流程图解

🧪 完整测试用例
1. 实体类配置
java
@Data
public class BaseEntity {
private Long tenantId;
private Long id;
private Date createTime;
private Date updateTime;
}
@Entity
@Table(name = "users")
public class User extends BaseEntity {
private String username;
private String email;
private String status;
}
2. 数据库索引策略
sql
-- 为tenant_id创建索引
CREATE INDEX idx_users_tenant_id ON users(tenant_id);
-- 复合索引将tenant_id放在首位
CREATE INDEX idx_users_tenant_status ON users(tenant_id, status);
CREATE INDEX idx_orders_tenant_created ON orders(tenant_id, created_at);
3.测试用例
java
@SpringBootTest
class TenantInterceptorTest {
@Autowired
private UserMapper userMapper;
@Test
void testTenantFilter() {
// 设置租户上下文
TenantContext.setCurrentTenant(123L);
List<User> users = userMapper.findByStatus("ACTIVE");
// 验证SQL中自动添加了tenant_id条件
assertThat(users).allMatch(user -> user.getTenantId().equals(123L));
}
@Test
void testSkipTenantFilter() {
TenantContext.setCurrentTenant(123L);
// 使用@SkipTenantFilter注解的方法应该返回所有数据
List<User> allUsers = userMapper.findAllForAdmin();
assertThat(allUsers).isNotEmpty();
}
}
💡 最佳实践总结
- 自动化的数据隔离:拦截器透明处理,业务代码无需关心租户隔离
- 灵活的配置策略:通过注解精细控制过滤行为
- 双重安全防护:应用层拦截 + 数据库行级安全
- 性能优先:合理的索引策略和缓存方案
- 完善的监控:日志记录和性能监控
🎯 适用场景
- 标准化SaaS产品:面向中小企业的通用SaaS服务
- 多租户管理系统:需要为不同客户隔离数据的后台系统
- 云服务平台:提供多租户能力的PaaS平台
🔚 结语
通过MyBatis 拦截器实现多租户数据隔离,我们构建了一个安全、高效、易维护的SaaS架构。这种方案不仅保证了数据的安全性,还提供了优秀的开发体验和系统性能。
主要优势:
- ✅ 开发效率高:业务代码无需关心租户隔离
- ✅ 维护成本低:集中化的拦截器管理
- ✅ 系统性能好:基于索引的高效查询
- ✅ 安全系数高:多重安全防护机制