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 实现细粒度的访问控制
-
定期审计和优化多租户系统