MyBatisPlus实现多租户功能

news2025/1/23 10:26:27

前言:多租户是一种软件架构技术,在多用户的环境下,共有同一套系统,并且要注意数据之间的隔离性。

一、SaaS多租户简介

1.1、SaaS多租户

  •  SaaS,是Software-as-a-Service的缩写名称,意思为软件即服务,即通过网络提供软件服务。

  •  SaaS平台供应商将应用软件统一部署在自己的服务器上,客户可以根据工作实际需求,通过互联网向厂商定购所需的应用软件服务,按定购的服务多少和时间长短向厂商支付费用,并通过互联网获得Saas平台供应商提供的服务。

  •  SaaS服务通常基于一套标准软件系统为成百上千的不同客户(又称为租户)提供服务。这要求SaaS服务能够支持不同租户之间数据和配置的隔离,从而保证每个租户数据的安全与隐私,以及用户对诸如界面、业务逻辑、数据结构等的个性化需求。由于SaaS同时支持多个租户,每个租户又有很多用户,这对支撑软件的基础设施平台的性能、稳定性和扩展性提出很大挑战。

1.2、多租户

  • 多租户技术(英语:multi-tenancy technology)或称多重租赁技术,是一种软件架构技术,它是在探讨与实现如何于多用户的环境下共用相同的系统或程序组件,并且仍可确保各用户间数据的隔离性。

  •  多租户技术可以实现多个租户之间共享系统实例,同时又可以实现租户的系统实例的个性化定制。通过使用多租户技术可以保证系统共性的部分被共享,个性的部分被单独隔离。通过在多个租户之间的资源复用,运营管理维护资源,有效节省开发应用的成本。

  •  多租户技术的实现重点,在于不同租户间应用程序环境的隔离(application context isolation)以及数据的隔离(data isolation),以维持不同租户间应用程序不会相互干扰,同时数据的保密性也够强。

1.3、多租户数据隔离有三种方案

  • 独立数据库:简单来说就是一个租户使用一个数据库,这种数据隔离级别最高,安全性最好,但是提高成本。

  • 共享数据库、隔离数据架构:多租户使用同一个数据裤,但是每个租户对应一个Schema(数据库user)。

  • 共享数据库、共享数据架构:使用同一个数据库,同一个Schema,但是在表中增加了租户ID的字段,这种共享数据程度最高,隔离级别最低。

如果希望以最少的服务器为最多的租户提供服务,并且租户接受以牺牲隔离级别换取降低成本。我们可以采用方案三,即共享数据库,共享数据架构,因为这种方案服务器成本最低,但是提高了开发成本。所以MybatisPlus就提供了一种多租户的解决方案,实现方式是基于多租户插件TenantLineInnerInterceptor进行实现的。


二、MybatisPlus多租户插件

MybatisPlus提供了租户处理器( TenantId 行级 ),租户之间共享数据库,共享数据架构,通过表字段(租户ID)进行数据逻辑隔离。

注意事项:

  • 多租户 != 权限过滤,不要乱用,租户之间是完全隔离的!!!
  • 启用多租户后所有执行的method的sql都会进行处理.
  • 自写的sql请按规范书写(sql涉及到多个表的每个表都要给别名,特别是 inner join 的要写标准的 inner join)

TenantLineInnerInterceptor是MybatisPlus中提供的多租户插件,其使用方法大致分为下面4步:

        <!-- Mybatis-Plus 增强CRUD -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>

        <!-- Mybatis-Plus 扩展插件 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-extension</artifactId>
            <version>3.5.1</version>
        </dependency>

2.1、表及实体类添加租户ID

租户ID一般用tenant_id

在这里插入图片描述

 2.2、application文件中添加多租户配置和新增配置属性类

(1)设置环境变量,配置拦截规则:可以设置是否开启多租户,对多租户的表设置白名单忽略多租户拦截等。

#多租户配置
tenant:
  enable: true
  column: tenant_id
  filterTables:
  ignoreTables:
    - sys_app
    - sys_config
    - sys_dict_data
    - sys_dict_type
    - sys_logininfor
    - sys_menu
    - sys_notice
    - sys_oper_log
    - sys_role
    - sys_role_menu
    - sys_user
    - sys_user_role
  ignoreLoginNames:

例如sys_user表结构中,没有tenant_id多租户字段,那么多租户拦截器不拦截该表。

(2)多租户配置属性类

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
 
/**
 * 多租户配置属性类
 *
 * @author hege
 * @Date 2023-08-25
 *
 */
@Data
@ConfigurationProperties(prefix = "tenant")
public class TenantProperties {
    /**
     * 是否开启多租户
     */
    private Boolean enable = true;
 
    /**
     * 租户id字段名
     */
    private String column = "tenant_id";
 
    /**
     * 需要进行租户id过滤的表名集合
     */
    private List<String> filterTables;
 
    /**
     * 需要忽略的多租户的表,此配置优先filterTables,若此配置为空则启用filterTables
     */
    private List<String> ignoreTables;
 
    /**
     * 需要排除租户过滤的登录用户名
     */
    private List<String> ignoreLoginNames;
}

2.3、编写多租户处理器实现TenantLineHandler接口

import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.NullValue;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.List;
 
/**
 * 多租户处理器实现TenantLineHandler接口
 *
 * @author hege
 * @Date 2023-08-25
 */
public class MultiTenantHandler implements TenantLineHandler {
 
    private final TenantProperties properties;
 
    public MultiTenantHandler(TenantProperties properties) {
        this.properties = properties;
    }
 
    /**
     * 获取租户ID值表达式,只支持单个ID值 (实际应该从用户信息中获取)
     *
     * @return 租户ID值表达式
     */
    @Override
    public Expression getTenantId() {

        //实际应该从用户信息中获取
        if(SecurityUtils.getTenantLoginUser()!=null)
        {
            Long tenantId = SecurityUtils.getLoginUser().getUser().getRootPartyId();
            if(tenantId!=null)
            {
                return new LongValue(tenantId);
            }
        }

        return new LongValue(0);

    }
 
    /**
     * 获取租户字段名,默认字段名叫: tenant_id
     *
     * @return 租户字段名
     */
    @Override
    public String getTenantIdColumn() {
        return properties.getColumn();
    }
 
    /**
     * 根据表名判断是否忽略拼接多租户条件
     *
     * 默认都要进行解析并拼接多租户条件
     *
     * @param tableName 表名
     * @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
     */
    @Override
    public boolean ignoreTable(String tableName) {
 
        //忽略指定用户对租户的数据过滤
        List<String> ignoreLoginNames=properties.getIgnoreLoginNames();
        String loginName=SecurityUtils.getTenantUsername();
        if(null!=ignoreLoginNames && ignoreLoginNames.contains(loginName)){
            return true;
        }

        //忽略指定表对租户数据的过滤
        List<String> ignoreTables = properties.getIgnoreTables();
        if (null != ignoreTables && ignoreTables.contains(tableName)) {
            return true;
        }
 
        return false;
    }
}

对应用户服务工具类

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 安全服务工具类
 *
 * @author hege
 */
public class SecurityUtils {

   
    /**
     * 获取多租户用户
     **/
    public static LoginUser getTenantLoginUser() {
        try {

            LoginUser loginUser = null;

            // 获取安全上下文对象,就是那个保存在ThreadLocal里面的安全上下文对象,总是不为null(如果不存在,则创建一个authentication属性为null的empty安全上下文对象)
            SecurityContext securityContext = SecurityContextHolder.getContext();
            // 获取当前认证了的 principal(当事人) 或者 request token (令牌); 如果没有认证,会是 null,该例子是认证之后的情况
            Authentication authentication = securityContext.getAuthentication();

            if(authentication!=null)
            {
                if(authentication.getPrincipal()!=null)
                {
                    if (authentication.getPrincipal() instanceof LoginUser) {
                        loginUser = (LoginUser) authentication.getPrincipal();
                    }

                }
            }
            return loginUser;

        } catch (Exception e) {
            e.printStackTrace();
            throw new ServiceException("获取用户信息异常", HttpStatus.UNAUTHORIZED);
        }
    }

}

2.4、MybatisPlus配置类启用多租户拦截插件

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
 * Mybatis Plus 配置
 *
 * @author hege
 */
@EnableTransactionManagement(proxyTargetClass = true)
@Configuration
@EnableConfigurationProperties(TenantProperties.class)
public class MybatisPlusConfig {

    /**
     * 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
     *
     * @param tenantProperties
     * @return
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(TenantProperties tenantProperties) {

        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        if (Boolean.TRUE.equals(tenantProperties.getEnable())) {
            // 启用多租户插件拦截
            interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new MultiTenantHandler(tenantProperties)));
        }

        // 分页插件
        interceptor.addInnerInterceptor(paginationInnerInterceptor());
        // 乐观锁插件
        interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());
        // 阻断插件
        interceptor.addInnerInterceptor(blockAttackInnerInterceptor());

        return interceptor;
    }

}

2.5、特定SQL语句忽略拦截

在一些场景下,无需多租户拦截,或者对于一些超级管理员使用的接口,希望跨租户查询、免数据鉴权时,可以通过下面几种方式实现忽略拦截:

  • 使用MybatisPlus框架自带的@InterceptorIgnore注解,以用在Mapper类上,也可以用在方法上

  • 添加超级用户账号白名单,在自定义的Handler里进行逻辑判断,跳过拦截

  • 添加数据表白名单,在自定义的Handler里进行逻辑判断,跳过拦截

 /**
     * 使用@InterceptorIgnore注解,忽略多租户拦截 <br/>
     * 注解@InterceptorIgnore可以用在Mapper类上,也可以用在方法上
     *
     * @param id
     * @return
     */
    @InterceptorIgnore(tenantLine = "true")
    UserOrgVO myFindByIdNoTenant(@Param(value = "id") Long id);

验证结果:

针对MybatisPlus提供的API、自定义Mapper中的statement均可正常拦截,会在SQL执行增删改查的时候自动加上tenant_id。


参考链接:

数据权限拦截器,多租户拦截器

Mybatis-Plus入门系列(4)- MybatisPlus之多租户插件TenantLineInnerInterceptor_mybatis plus tenant

使用MyBatisPlus实现多租户功能

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/942686.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【电源专题】读一读单节锂电池保护IC规格书

在文章【电源专题】单节锂离子电池的保护的基本原理 中我们了解了电池包的过充、过放、过流、短路等保护功能。那么这些功能都会在电池保护IC规格书中体现吗?体现在哪些地方?哪些参数是我们应关注的呢? 对于手册中的电压检测,如放电过流、充电过流和负载短路等检测电压都代…

开源软件的国际化和本地化

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

centos7删除乱码文件

centos7删除乱码文件1. 小白教程&#xff0c;一看就会&#xff0c;一做就成。 1.解释 当文件名为乱码的时候&#xff0c;无法通过键盘输入文件名&#xff0c;所以在终端下就不能直接利用rm&#xff0c;mv等命令管理文件了。 但是每个文件都有一个i节点号&#xff0c;可以通过…

《Flink学习笔记》——第三章 Flink的部署模式

不同的应用场景&#xff0c;有时候对集群资源的分配和占用有不同的需求。所以Flink为各种场景提供了不同的部署模式。 3.1 部署模式&#xff08;作业角度/通用分类&#xff09; 根据集群的生命周期、资源的分配方式、main方法到底在哪里执行——客户端还是Client还是JobManage…

AIGC - 生成模型

AIGC - 生成模型 0. 前言1. 生成模型2. 生成模型与判别模型的区别2.1 模型对比2.2 条件生成模型2.3 生成模型的发展2.4 生成模型与人工智能 3. 生成模型示例3.1 简单示例3.2 生成模型框架 4. 表示学习5. 生成模型与概率论6. 生成模型分类小结 0. 前言 生成式人工智能 (Generat…

【最强最全】视频号下载助手(支持视频号视频, 直播,回放下载)

视频号下载助手支持视频号视频, 直播,回放的下载&#xff0c;本工具基于秦天sunny中间件编写&#xff0c;无需再使用其它抓包软件&#xff0c;无需再使用其它下载软件。 当然&#xff0c;你也可以右键复制抓取后的视频源再用其它下载软件下载。 使用说明 解压文件&#xff0c;…

CSS中如何实现弹性盒子布局(Flexbox)的换行和排序功能?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 换行&#xff08;Flexbox Wrapping&#xff09;⭐ 示例&#xff1a;实现换行⭐ 排序&#xff08;Flexbox Ordering&#xff09;⭐ 示例&#xff1a;实现排序⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得…

基于java swing和mysql实现的仓库商品管理系统(源码+数据库+运行指导视频)

一、项目简介 本项目是一套基于java swing和mysql实现的仓库商品管理系统&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Java学习者。 包含&#xff1a;项目源码、项目文档、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经…

流媒体弱网优化之路(BBR应用)——GCC与BBR的算法思想分析

流媒体弱网优化之路(WebRTC)——GCC与BBR的算法思想分析 —— 我正在的github给大家开发一个用于做实验的项目 —— github.com/qw225967/Bifrost目标&#xff1a;可以让大家熟悉各类Qos能力、带宽估计能力&#xff0c;提供每个环节关键参数调节接口并实现一个json全配置&…

【洛谷算法题】P1001-A+B Problem【入门1顺序结构】

&#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【洛谷算法题】 文章目录 【洛谷算法题】P1001-AB Problem【入门1顺序结构】&#x1f30f;题目背景&#x1f30f;题目描述…

【Linux操作系统】Linux系统编程中条件变量实现生产者消费者模型

在Linux系统编程中&#xff0c;条件变量是一种用于线程间同步的机制&#xff0c;常用于实现生产者消费者模型。生产者消费者模型是一种常见的并发编程模型&#xff0c;用于解决多线程环境下的数据共享和同步问题。在该模型中&#xff0c;生产者负责生产数据&#xff0c;消费者负…

53 个 CSS 特效 3(完)

53 个 CSS 特效 3&#xff08;完&#xff09; 前两篇地址&#xff1a; 53 个 CSS 特效 153 个 CSS 特效 2 这里是第 33 到 53 个&#xff0c;很多内容都挺重复的&#xff0c;所以这里解释没之前的细&#xff0c;如果漏了一些之前的笔记会补一下&#xff0c;写过的就会跳过。…

【算法训练-模拟】模拟设计LRU缓存结构

废话不多说&#xff0c;喊一句号子鼓励自己&#xff1a;程序员永不失业&#xff0c;程序员走向架构&#xff01;本篇Blog的主题是LRU缓存结构设计&#xff0c;这类题目出现频率还是很高的&#xff0c;几乎所有大厂都常考。 当然面对这道题&#xff0c;首先要讲清楚LRU是干什么…

JavaScript—对象与构造方法

目录 json对象&#xff08;字面值&#xff09; js中对象是什么&#xff1f; 如何使用&#xff1f; 关联数组 js对象和C#对象有什么区别&#xff1f; 构造函数 什么是构造方法&#xff1f; 如何使用构造方法&#xff1f; 如何添加成员&#xff1f; 对象的动态成员 正则…

PageObject三层架构模式实现

1&#xff1a;PageObject三层架构分为&#xff1a; 接下来用163邮箱的登录功能来举例说明三层架构的使用。 1&#xff1a;先创建目录结构&#xff0c;如下图 2&#xff1a;在工具Util中&#xff0c;先封装查找元素定位的工具&#xff0c;创建一个find_ele.py文件。内容如下&am…

JavaScript—DOM(文档对象模型)

目录 DOM是什么&#xff1f; DOM有什么作用&#xff1f; 一、事件 理解事件 事件怎么写&#xff08;要做什么就写什么&#xff09;&#xff1f; 实战演练 1、页面加载完毕以后&#xff0c;打印一句话 2、如果有一个a标签&#xff0c;并给其添加一个点击事件 3、事件默…

电脑如何投屏到手机?Windows投屏到iPhone也可以吗?

我们知道&#xff0c;因为各大品牌厂商越来越维护自己的名声&#xff0c;都会推出“全家桶”&#xff0c;就是某些功能&#xff0c;你在使用同一品牌的电脑、手机、平板时非常好用&#xff0c;但一旦跨品牌就用不了。电脑投屏到手机也会遇到这种“品牌隔离”。 如果参会人使用…

对DataFrame对象中的数据将各行列进行整体平移DataFrame.shift()

【小白从小学Python、C、Java】 【计算机等级考试500强双证书】 【Python-数据分析】 对DataFrame对象中的数据 将各行列进行整体平移 DataFrame.shift() [太阳]选择题 以下python代码错误的是? import pandas as pd dfpd.DataFrame({A:[1,2,3],B:[4,5,6]}) print(【显示】df&…

SAP MM学习笔记26- SAP中 振替转记(转移过账)和 在库转送(库存转储)3- Plant间在库转送

SAP 中在库移动 不仅有入库&#xff08;GR&#xff09;&#xff0c;出库&#xff08;GI&#xff09;&#xff0c;也可以是单纯内部的转记或转送。 1&#xff0c;振替转记&#xff08;转移过账&#xff09; 2&#xff0c;在库转送&#xff08;库存转储&#xff09; 1&#xff…