MyBatisPlus实现多租户功能

news/2024/11/24 14:06:44/

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

一、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步:

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

租户ID一般用tenant_id

在这里插入图片描述

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

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

#多租户配置
tenant:enable: truecolumn: tenant_idfilterTables: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_roleignoreLoginNames:

例如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 com.ysgz.common.utils.SecurityUtils;
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值表达式*/@Overridepublic 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 租户字段名*/@Overridepublic String getTenantIdColumn() {return properties.getColumn();}/*** 根据表名判断是否忽略拼接多租户条件** 默认都要进行解析并拼接多租户条件** @param tableName 表名* @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件*/@Overridepublic 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;}
}

对应用户服务工具类

package com.ysgz.common.utils;import com.ysgz.common.core.domain.entity.SysRole;
import com.ysgz.common.core.domain.entity.SysUser;
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 com.ysgz.common.constant.HttpStatus;
import com.ysgz.common.core.domain.model.LoginUser;
import com.ysgz.common.exception.ServiceException;
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配置类启用多租户拦截插件

package com.ysgz.framework.config;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 com.ysgz.framework.tenant.MultiTenantHandler;
import com.ysgz.framework.tenant.TenantProperties;
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*/@Beanpublic 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.ppmy.cn/news/1067108.html

相关文章

leetcode分类刷题:滑动窗口(三、两个序列+窗口定长类型)

1、通过对滑动窗口前两个题型的总结&#xff0c;我们几乎已经习惯在给定的一个序列里使用滑动窗口的模板解题了&#xff0c;本次对应的“三、两个序列窗口定长类型”&#xff0c;也是考察连续子数组、连续子串问题&#xff0c;只不过这次会给定两个序列&#xff0c;判断短序列在…

1.4 编写简易ShellCode弹窗

在前面的章节中相信读者已经学会了使用Metasploit工具生成自己的ShellCode代码片段了&#xff0c;本章将继续深入探索关于ShellCode的相关知识体系&#xff0c;ShellCode 通常是指一个原始的可执行代码的有效载荷&#xff0c;攻击者通常会使用这段代码来获得被攻陷系统上的交互…

proxysql使用心得

proxySQL 多层配置系统结构 -------------------------| RUNTIME |-------------------------/|\ || |[1] | [2] || \|/-------------------------| MEMORY |------------------------- _/|\ | …

玩转Mysql系列 - 第9篇:分组查询详解(group by having)

这是Mysql系列第9篇。 环境&#xff1a;mysql5.7.25&#xff0c;cmd命令中进行演示。 本篇内容 分组查询语法 聚合函数 单字段分组 多字段分组 分组前筛选数据 分组后筛选数据 where和having的区别 分组后排序 where & group by & having & order by &am…

postgresql 性能调优

性能调优是为了提高 PostgreSQL 数据库的性能和响应速度。下面是一些常见的 PostgreSQL 性能调优技巧&#xff1a; 1 确保合适的硬件资源&#xff1a;确保数据库服务器具有足够的内存、处理器和磁盘空间&#xff0c;以满足数据库负载的需求。2 优化查询语句&#xff1a;检查并优…

解析代理IP在跨境电商和社媒营销中的关键作用

跨境电商和社媒营销领域的从业者深知&#xff0c;代理IP的价值愈发凸显。在推广营销的过程中&#xff0c;频繁遇到因IP关联而封禁账号的情况&#xff0c;或因使用不安全IP而导致异常问题。 这些问题促使人们开始高度重视代理IP的作用。但实际上&#xff0c;代理IP究竟是何物&a…

Java匿名内部类

文章目录 前言一、使用匿名内部类需要注意什么&#xff1f;二、使用步骤匿名内部类的结构匿名内部类的实用场景1. 事件监听器2. 过滤器3. 线程4. 实现接口5.单元测试&#xff1a;6.GUI编程7.回调函数 前言 Java中的匿名内部类是一种可以在声明时直接创建对象的内部类。这种内部…

使用Python爬虫采集网络热点

在当今信息爆炸的时代&#xff0c;了解网络热搜词和热点事件对于我们保持时事敏感性和把握舆论动向非常重要。在本文中&#xff0c;我将与你分享使用Python爬虫采集网络热搜词和热点事件的方法&#xff0c;帮助你及时获取热门话题和热点新闻。 1. 网络热搜词采集 网络热搜词是人…