多租户实现 基于SpringBoot+MybtaisPlus ~ 行级别隔离实现

目录

目标:根据租户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


目标:根据租户id【tenant_id】字段实现数据隔离

为方便以后使用 将多租户处理封装为独立的Starter【要求starter使用项目已集成mybatisPlus】

版本:SpringBoot ~ 2.7.10

          MybatisPlus ~ 3.5.2

 mybatisPlus 低版本多租户处理有缺陷:

        如:3.1.1版本 针对如下场景无效: 3.2/3.3/3.4版本未验证,因多表关联bug是在3.5.0版本后修复的

 场景①、from后的表为无需租户处理时,处理失效

  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 时,如果插入语句中不可包含【tenant_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')
场景③、多表关联Bug
# 当多租户处理表为关联表时,添加租户过滤条件的位置是在 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; 

1、Starter封装

①、pom.xml 依赖



    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
        

    

②、Properties 定义:

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;
    }
}

③、TenantContext 租户上下文

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

}

④、AutoConfiguration配置

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

⑤、基于过滤器、拦截器获取请求参数租户id值

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

}

⑥、spring.factories 【路径:resources/META-INF/spring.factories】

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ap.config.MultiTenantAutoConfiguration

2、Starter使用

①、pom.xml 依赖

        
        
            com.ap
            multi-tenant-spring-boot-starter
            1.0-SNAPSHOT
        

        
            com.baomidou
            mybatis-plus-boot-starter
            3.5.2
       

②、yml 配置


# 多租户
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"
        );
    }
}

⑤、忽略某条sql

在mapper类中需要忽略的sql方法上增加 注解 :@InterceptorIgnore(tenantLine = "1")

你可能感兴趣的:(spring,boot,后端,java)