目录
目标:根据租户id【tenant_id】字段实现数据隔离
mybatisPlus 低版本多租户处理有缺陷:
场景①、from后的表为无需租户处理时,处理失效
场景②、insert 时,如果插入语句中不可包含【tenant_id】字段,否则插入异常
场景③、多表关联Bug
1、Starter封装
①、pom.xml 依赖
②、Properties 定义:
③、TenantContext 租户上下文
④、AutoConfiguration配置
⑤、基于过滤器、拦截器获取请求参数租户id值
⑥、spring.factories 【路径:resources/META-INF/spring.factories】
2、Starter使用
①、pom.xml 依赖
②、yml 配置
③、过滤器启用
④、拦截器启用
⑤、忽略某条sql
为方便以后使用 将多租户处理封装为独立的Starter【要求starter使用项目已集成mybatisPlus】
版本:SpringBoot ~ 2.7.10
MybatisPlus ~ 3.5.2
如:3.1.1版本 针对如下场景无效: 3.2/3.3/3.4版本未验证,因多表关联bug是在3.5.0版本后修复的
user_addr 无需租户处理 、sys_user 需要租户处理的表
# 3.1.1
SELECT a.name AS addr_name, u.id, u.name
FROM user_addr a
LEFT JOIN sys_user u ON a.user_id = u.id
INSERT INTO sys_user (id, tenant_id, name) VALUES
(1, 1, 'xxxx')
# 处理后 造成异常
INSERT INTO sys_user (id, tenant_id, name, tenant_id) VALUES
(1, 1, 'xxxx', 'xx')
# 当多租户处理表为关联表时,添加租户过滤条件的位置是在 ON 关联条件上 造成租户筛选失效
SELECT a.name AS addr_name, u.id, u.name,u.tenant_id
FROM user_addr a
RIGHT JOIN sys_user u ON u.id = a.user_id and u.tenant_id = 1;
# 3.5.0版本后修复此bug
SELECT a.name AS addr_name, u.id, u.name,u.tenant_id
FROM user_addr a
RIGHT JOIN sys_user u ON u.id = a.user_id
where u.tenant_id = 1;
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.7.10
com.ap
multi-tenant-spring-boot-starter
1.0-SNAPSHOT
8
8
UTF-8
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-autoconfigure
org.springframework.boot
spring-boot-configuration-processor
true
com.baomidou
mybatis-plus
3.5.2
org.springframework
spring-web
provided
javax.servlet
javax.servlet-api
provided
org.springframework
spring-webmvc
provided
package com.ap.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
/**
* 类名:MultiTenantProperties.java
* 描述:多租户配置
*
* @author AP
* @version 1.0
* @date 2023/7/12 10:02
*/
@ConfigurationProperties(prefix = MultiTenantProperties.PREFIX)
public class MultiTenantProperties {
public static final String PREFIX = "multi-tenant";
/**
* 是否启用
*/
private boolean enabled;
/**
* 租户id列名称
*/
private String tenantIdColumn;
/**
* 忽略多租户限制条件的库表名
*/
private List ignoreTables;
/**
* request請求header 中的多租户标识
*/
private String headerTenantId;
public String getHeaderTenantId() {
return headerTenantId;
}
public void setHeaderTenantId(String headerTenantId) {
this.headerTenantId = headerTenantId;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getTenantIdColumn() {
return tenantIdColumn;
}
public void setTenantIdColumn(String tenantIdColumn) {
this.tenantIdColumn = tenantIdColumn;
}
public List getIgnoreTables() {
return ignoreTables;
}
public void setIgnoreTables(List ignoreTables) {
this.ignoreTables = ignoreTables;
}
}
package com.ap.context;
/**
* 类名:TenantContextHolder.java
* 描述:
*
* @author AP
* @version 1.0
* @date 2023/7/12 16:43
*/
/**
* 多租户上下文
*/
public class TenantContext {
/**
* 当前租户编号
* # 采用 InheritableThreadLocal 防止多线程处理时子线程无法获取到租户id
*/
private static final ThreadLocal TENANT_ID = new InheritableThreadLocal<>();
/**
* 是否忽略租户
* # 采用 InheritableThreadLocal 防止多线程处理时子线程无法获取到租户id
*/
private static final ThreadLocal IGNORE = new InheritableThreadLocal<>();
/**
* 获得租户编号。
*
* @return 租户编号
*/
public static String getTenantId() {
return TENANT_ID.get();
}
/**
* 获得租户编号。如果不存在,则抛出 NullPointerException 异常
*
* @return 租户编号
*/
public static String getRequiredTenantId() {
String tenantId = getTenantId();
if (tenantId == null) {
throw new NullPointerException("TenantContext 不存在租户编号");
}
return tenantId;
}
public static void setTenantId(String tenantId) {
TENANT_ID.set(tenantId);
}
public static void setIgnore(Boolean ignore) {
IGNORE.set(ignore);
}
/**
* 当前是否忽略租户
*
* @return 是否忽略
*/
public static boolean isIgnore() {
return Boolean.TRUE.equals(IGNORE.get());
}
public static void clear() {
TENANT_ID.remove();
IGNORE.remove();
}
}
package com.ap.config;
/**
* 类名:ExampleAutoConfiguration.java
* 描述:
*
* @author AP
* @version 1.0
* @date 2023/7/12 10:12
*/
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.troila.context.TenantContext;
import com.troila.properties.MultiTenantProperties;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.CollectionUtils;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
@Configuration
@EnableConfigurationProperties(MultiTenantProperties.class)
@ConditionalOnProperty(prefix = MultiTenantProperties.PREFIX, value = "enabled", havingValue = "true")
public class MultiTenantAutoConfiguration {
//@Autowired(required = false)
@Resource
MybatisPlusInterceptor mybatisPlusInterceptor;
@Resource(type=MultiTenantProperties.class)
MultiTenantProperties properties;
@PostConstruct
public void tenantLineInnerInterceptor(){
/*if (mybatisPlusInterceptor == null) {
mybatisPlusInterceptor = new MybatisPlusInterceptor();
}*/
mybatisPlusInterceptor.addInnerInterceptor(
new TenantLineInnerInterceptor(
new TenantLineHandler() {
@Override
public Expression getTenantId() {
return new StringValue(TenantContext.getRequiredTenantId());
}
// 租户id 列名值
@Override
public String getTenantIdColumn(){
return properties.getTenantIdColumn();
}
// 返回 false 表示所有表都需要拼多租户条件
@Override
public boolean ignoreTable(String tableName) {
List ignoreTables = properties.getIgnoreTables();
return !CollectionUtils.isEmpty(ignoreTables) && ignoreTables.contains(tableName);
}
}
)
);
}
}
package com.ap.filter;
import com.troila.context.TenantContext;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 类名:TenantContextWebFilter.java
* 描述: 多租户 Context Web 过滤器
*
* @author AP
* @version 1.0
* @date 2023/7/12 16:45
*/
public class TenantContextFilter extends OncePerRequestFilter {
// 也可注入 MultiTenantProperties 获取
@Value("${multi-tenant.header-tenant-id}")
private String headerTenantId;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 设置
String tenantId = request.getHeader(headerTenantId);
if (StringUtils.hasText(tenantId)) {
TenantContext.setTenantId(tenantId);
}
try {
chain.doFilter(request, response);
} finally {
// 清理
TenantContext.clear();
}
}
}
package com.ap.interceptor;
import com.troila.context.TenantContext;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 类名:TenantContextInterceptor.java
* 描述:多租户 mvc 拦截器
*
* @author AP
* @version 1.0
* @date 2023/7/12 17:32
*/
public class TenantContextInterceptor implements HandlerInterceptor {
// 也可注入 MultiTenantProperties 获取
@Value("${multi-tenant.header-tenant-id}")
private String headerTenantId;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String tenantId = request.getHeader(headerTenantId);
if (StringUtils.hasText(tenantId)) {
TenantContext.setTenantId(tenantId);
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex){
TenantContext.clear();
}
}
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ap.config.MultiTenantAutoConfiguration
com.ap
multi-tenant-spring-boot-starter
1.0-SNAPSHOT
com.baomidou
mybatis-plus-boot-starter
3.5.2
# 多租户
multi-tenant:
# 是否开启多租户处理 true 开启 false 关闭
enabled: true
# 表对应的租户字段名称
tenant-id-column: "tenant_id"
# 请求头中租户key值
header-tenant-id: "tenant-id"
# 忽略表
ignore-tables:
- message
- user
在启动类或配置类采用 @import 注解启用
@Import(TenantContextFilter.class)
package com.ap1.config;
/**
* 类名:WebMvcConfig.java
* 描述:
*
* @author AP
* @version 1.0
* @date 2023/7/13 16:47
*/
import com.troila.interceptor.TenantContextInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
// 导入多租户的Starter拦截器
@Import(TenantContextInterceptor.class)
public class WebMvcConfig implements WebMvcConfigurer {
@Resource
TenantContextInterceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册拦截器
InterceptorRegistration registration = registry.addInterceptor(interceptor);
registration.addPathPatterns("/**");//所有路径都被拦截
registration.excludePathPatterns(//添加不拦截路径
"你的登陆路径",//登录
"/**/*.html",//html静态资源
"/**/*.js",//js静态资源
"/**/*.css",//css静态资源
"/**/*.woff",
"/**/*.ttf"
);
}
}
在mapper类中需要忽略的sql方法上增加 注解 :@InterceptorIgnore(tenantLine = "1")