mthoutai

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

前言

在当今云计算时代,SaaS(Software as a Service) 模式已经成为企业服务的主流形态。作为SaaS系统的核心架构要素,多租户技术能够让单个应用实例为多个客户(租户)提供服务,同时确保各租户数据的安全隔离。

本文将深入探讨如何使用 MyBatis拦截器实现高效、安全的行级数据隔离方案,为正在构建或优化SaaS系统的开发者提供完整的技术解决方案。

️ 多租户数据隔离方案对比

在SaaS系统中,数据隔离主要有三种实现方案:

隔离级别实现方式优点缺点适用场景
数据库级每个租户独立数据库隔离性最高、安全性最好成本高、扩展性差金融、政府等高安全要求
Schema级共享数据库,独立Schema良好隔离性、中等成本扩展性受限中大型企业客户
行级共享数据库和Schema成本最低、扩展性最佳依赖应用层安全标准化SaaS产品

行级数据隔离 因其成本效益可扩展性成为大多数SaaS企业的首选方案。其核心原理是在所有业务表中添加tenant_id字段,通过SQL拦截自动添加租户过滤条件。

MyBatis拦截器核心实现

环境准备

首先在pom.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. 租户上下文管理

@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层租户拦截器

@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多租户拦截器(核心代码)

@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. 注解定义

// 跳过租户过滤注解
@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. 注解驱动拦截器

@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使用示例

@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();
      }

执行流程图解

mybatis拦截sql执行流程

完整测试用例

1. 实体类配置

@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. 数据库索引策略

-- 为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.测试用例

@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();
    }
    }

最佳实践总结

  1. 自动化的数据隔离:拦截器透明处理,业务代码无需关心租户隔离
  2. 灵活的配置策略:通过注解精细控制过滤行为
  3. 双重安全防护:应用层拦截 + 数据库行级安全
  4. 性能优先:合理的索引策略和缓存方案
  5. 完善的监控:日志记录和性能监控

适用场景

  • 标准化SaaS产品:面向中小企业的通用SaaS服务
  • 多租户管理系统:需要为不同客户隔离数据的后台系统
  • 云服务平台:提供多租户能力的PaaS平台

结语

通过MyBatis拦截器实现多租户数据隔离,我们构建了一个安全、高效、易维护的SaaS架构。这种方案不仅保证了数据的安全性,还提供了优秀的开发体验和系统性能。

主要优势:

  • ✅ 开发效率高:业务代码无需关心租户隔离
  • ✅ 维护成本低:集中化的拦截器管理
  • ✅ 系统性能好:基于索引的高效查询
  • ✅ 安全系数高:多重安全防护机制
posted on 2025-11-06 13:31  mthoutai  阅读(74)  评论(0)    收藏  举报