MyBatis Plus 是一个强大的 Java ORM 框架,它简化了 CRUD 操作。其高级功能之一是多租户支持,这对于需要处理数据隔离和多租户访问控制的应用程序至关重要。以下是关于 MyBatis Plus 多租户支持的详细指南,说明如何实现数据隔离和多租户访问控制。
多租户概述
多租户是一种软件架构,允许多个租户(通常是不同的客户或组织)共享同一个应用程序实例,同时确保他们的数据彼此隔离。多租户架构在 SaaS(软件即服务)应用中尤为常见。
数据隔离和访问控制的重要性
在多租户环境中,数据隔离确保了每个租户只能访问属于自己的数据,防止数据泄露和安全问题。访问控制则是为了确保不同租户和用户具有适当的权限,避免未经授权的访问。
简要介绍 MyBatis Plus
MyBatis Plus 是在 MyBatis 基础上扩展的一个增强工具包,提供了更简单和高效的 CRUD 操作,同时支持多种高级特性,如分页、多租户等。
2. 多租户概念
多租户的定义
多租户架构是一种允许多个租户共享同一系统或应用资源的设计模式,但每个租户的数据和配置是相互隔离的。
多租户的类型
数据库级多租户:每个租户拥有独立的数据库。
模式级多租户:所有租户共享同一个数据库,但每个租户有独立的模式(Schema)。
表级多租户:所有租户共享同一个数据库和模式,通过表中的租户 ID 来区分数据。
实现多租户的挑战
数据隔离的复杂性
性能影响
数据迁移和备份
安全性和合规性
3. MyBatis Plus 多租户支持
配置 MyBatis Plus 的多租户支持
MyBatis Plus 提供了多租户插件,可以通过简单配置实现多租户支持。
@Configuration public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 添加多租户插件 TenantLineInnerInterceptor tenantLineInnerInterceptor = new TenantLineInnerInterceptor(); tenantLineInnerInterceptor.setTenantLineHandler(new TenantLineHandler() { @Override public Expression getTenantId() { // 返回当前租户ID return new LongValue(YourTenantContext.getCurrentTenantId()); } @Override public boolean ignoreTable(String tableName) { // 忽略多租户的表 return "some_table".equalsIgnoreCase(tableName); } }); interceptor.addInnerInterceptor(tenantLineInnerInterceptor); return interceptor; } }
多租户插件的使用
多租户插件通过拦截SQL语句,在查询条件中自动添加租户ID,从而实现数据隔离。
基于租户 ID 的数据过滤
在实际应用中,可以通过上下文或线程变量存储当前租户ID,并在查询时自动添加到SQL条件中。
4. 数据隔离
数据隔离的实现
数据隔离通过在每个表中添加租户ID列,并在查询时自动过滤租户ID实现。
如何确保每个租户的数据独立
每个租户独立的租户 ID
自动添加租户 ID 的查询和更新
严格的权限控制
实践中的常见问题和解决方案
SQL 注入防护
多租户数据备份和恢复
租户间数据迁移
5. 多租户访问控制
基于角色的访问控制(RBAC)
通过角色和权限管理,实现对不同租户和用户的细粒度访问控制。
如何为不同租户设置不同的权限
定义租户和用户的角色
分配角色权限
动态权限校验
访问控制策略的实现
使用注解或 AOP 实现权限检查
在服务层或控制器层实现权限校验
6. 实战案例
配置示例
首先,配置 MyBatis Plus 多租户支持:
package com.example.config; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 添加多租户插件 TenantLineInnerInterceptor tenantLineInnerInterceptor = new TenantLineInnerInterceptor() { @Override public Expression getTenantId() { // 返回当前租户ID return new LongValue(TenantContext.getCurrentTenantId()); } @Override public boolean ignoreTable(String tableName) { // 忽略多租户的表(比如公共表) return "common_table".equalsIgnoreCase(tableName); } }; interceptor.addInnerInterceptor(tenantLineInnerInterceptor); return interceptor; } }
租户上下文
接下来,定义一个租户上下文来存储当前租户 ID:
package com.example.context; public class TenantContext { private static final ThreadLocal<Long> TENANT_ID = new ThreadLocal<>(); public static void setCurrentTenantId(Long tenantId) { TENANT_ID.set(tenantId); } public static Long getCurrentTenantId() { return TENANT_ID.get(); } public static void clear() { TENANT_ID.remove(); } }
拦截器配置
为了确保每个请求都能正确设置和清除租户 ID,可以使用拦截器:
package com.example.interceptor; import com.example.context.TenantContext; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Component public class TenantInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 从请求头中获取租户ID并设置到上下文中 String tenantId = request.getHeader("X-Tenant-ID"); if (tenantId != null) { TenantContext.setCurrentTenantId(Long.valueOf(tenantId)); } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 清除租户ID TenantContext.clear(); } }
拦截器注册
将拦截器注册到 Spring 的拦截器链中:
package com.example.config; import com.example.interceptor.TenantInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private TenantInterceptor tenantInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(tenantInterceptor).addPathPatterns("/**"); } }
示例实体和Mapper
定义一个实体类和对应的 Mapper 接口:
package com.example.entity; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; @Data @TableName("user") public class User { @TableId private Long id; private Long tenantId; private String name; } package com.example.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.entity.User; import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserMapper extends BaseMapper<User> { }
服务层和控制器层
实现服务层和控制器层来处理业务逻辑:
package com.example.service; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.entity.User; import com.example.mapper.UserMapper; import org.springframework.stereotype.Service; @Service public class UserService extends ServiceImpl<UserMapper, User> { } package com.example.controller; import com.example.entity.User; import com.example.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/users") public class UserController { @Autowired private UserService userService; @GetMapping public List<User> getUsers() { return userService.list(); } @PostMapping public User createUser(@RequestBody User user) { userService.save(user); return user; } }
测试
测试时,确保在请求头中包含 X-Tenant-ID
,例如:
curl -H "X-Tenant-ID: 1" http://localhost:8080/users
7. 总结
主要要点回顾
多租户的定义和类型
MyBatis Plus 多租户支持的配置和使用
数据隔离和访问控制的实现
最佳实践
使用 MyBatis Plus 提供的多租户插件
结合 RBAC 实现细粒度的访问控制
定期审计和优化多租户系统