0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

求一种SaaS模式下多租户系统数据隔离实现方案

jf_ro2CN3Fa 来源:稀土掘金 2023-07-27 10:34 次阅读

1.背景

开发过SaaS系统平台的小伙伴一定对多租户这个概念不陌生,简单来说一个租户就是一个公司客户,多个租户共用同一个SaaS系统,一旦SaaS系统不可用,那么所有的租户都不可用。你可以这么理解SaaS系统就像一栋大楼,而租户就是大楼里面租办公楼层的公司,平时每家公司做着自己的业务,互不干扰,但是一旦大楼的电梯坏了,那么影响到的就是所有的公司。

多租户问题,其是一种架构设计方式,就是在一台或者一组服务器上运行的SaaS系统,可以为多个租户(客户)提供服务,目的是为了让多个租户在互联网环境下使用同一套程序,且保证租户间的数据隔离。

从这种架构设计的模式上,不难看出来,多租户架构的重点就是同一套程序下多个租户数据的隔离。由于租户数据是集中存储的,所以要实现数据的安全性,就是看能否实现对租户数据的隔离,防止租户数据不经意或被他人恶意地获取和篡改。在讲多租户数据隔离实现之前,先来看看什么是SaaS系统。

什么是SaaS系统

SaaS平台是运营saas软件的平台。SaaS提供商为企业搭建信息化所需要的所有网络基础设施及软件、硬件运作平台,并负责所有前期的实施、后期的维护等一系列服务,租户(企业)无需购买软硬件、建设机房、招聘IT人员,即可通过互联网使用信息系统。SaaS 是一种软件布局模型,其应用专为网络交付而设计,便于用户通过互联网托管、部署及接入。

简单来说就是租户给SaaS平台付租金就能使用平台提供的功能服务,当下比较典型就是各种云平台、云服务厂商

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

项目地址:https://github.com/YunaiV/ruoyi-vue-pro

视频教程:https://doc.iocoder.cn/video/

2.多租户数据隔离架构设计

目前saas多租户系统的数据隔离有三种架构设计,即为每个租户提供独立的数据库、独立的表空间、按字段区分租户,每种方案都有其各自的适用情况。

一个租户独立一个数据库

一个租户独立使用一个数据库,那就意味着我们的SaaS系统需要连接多个数据库,这种实现方案其实就和分库分表架构设计是一样的,好处就是数据隔离级别高、安全性好,毕竟一个租户单用一个数据库,但是物理硬件成本,维护成本也变高了。

独立的表空间

这种方案的实现方式,就是所有租户共用一个数据库系统,但是每个租户在数据库系统中拥有一个独立的表空间。

按租户id字段隔离租户

这种方案是多租户方案中最简单的数据隔离方法,即在每张表中都添加一个用于区分租户的字段(如tenant_id或org_id啥的)来标识每条数据属于哪个租户,当进行查询的时候每条语句都要添加该字段作为过滤条件,其特点是所有租户的数据全都存放在同一个表中,数据的隔离性是最低的,完全是通过字段来区分的,很容易把数据搞串或者误操作。

三种数据隔离架构设计的对比如下:

隔离方案 成本 支持租户数量 优点 缺点
独立数据库系统 数据隔离级别高,安全性,可以针对单个租户开发个性化需求 数据库独立安装,物理成本和维护成本都比较高
独立的表空间 较多 提供了一定程度的逻辑数据隔离,一个数据库系统可支持多个租户 数据库管理比较困难,表繁多,同时数据修复稍复杂
按租户id字段区分 维护和购置成本最低,每个数据库能够支持的租户数量最多 隔离级别最低,安全性也最低

大部分公司都是采用第三种:按租户id字段隔离租户 架构设计实现多租户数据隔离的。接下来我们就来看看代码层面怎么实现多租户数据隔离的。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

项目地址:https://github.com/YunaiV/yudao-cloud

视频教程:https://doc.iocoder.cn/video/

3.mybatis-plus优雅实现多租户数据权限隔离

上面我们说过按租户id字段隔离租户 这种方式就是在获取数据的时候对每一条SQL语句添加租户id作为过滤条件来隔离租户数据的。但是这样意味着每个查询SQL都必须加上租户id这个过滤条件,如果漏加就意味着会查询出不同租户的数据,这是绝对不允许的,同时每个查询接口都需要手动设置过滤条件,重复劳动,一点都不够优雅。这时候就不得不说说mybatis-plus的多租户插件了,看看它如何优雅实现多租户隔离的?

再讲述之前,我们先思考一下如何优雅实现数据隔离?首先我们要求每一条SQL都加上租户id这个过滤条件,这意味着我们需要解析原始SQL在合适的地方加上租户id过滤条件,我们知道mybatis提供扩展点就是拦截器,可以对SQL语句处理前后进行增强逻辑,分页插件就是这么做的,所以我们这里要增强SQL自然也是这样,接下来我们就来看看mybatis-plus多租户插件是怎么实现多租户数据隔离的。

该拦截器部分源码如下:

publicclassTenantLineInnerInterceptorextendsJsqlParserSupportimplementsInnerInterceptor{
//多租户处理器
privateTenantLineHandlertenantLineHandler;

//改SQL,添加多租户id条件
publicvoidbeforeQuery(Executorexecutor,MappedStatementms,Objectparameter,RowBoundsrowBounds,ResultHandlerresultHandler,BoundSqlboundSql)throwsSQLException{
if(!InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())){
MPBoundSqlmpBs=PluginUtils.mpBoundSql(boundSql);
mpBs.sql(this.parserSingle(mpBs.sql(),(Object)null));
}
}

publicvoidbeforePrepare(StatementHandlersh,Connectionconnection,IntegertransactionTimeout){
MPStatementHandlermpSh=PluginUtils.mpStatementHandler(sh);
MappedStatementms=mpSh.mappedStatement();
SqlCommandTypesct=ms.getSqlCommandType();
if(sct==SqlCommandType.INSERT||sct==SqlCommandType.UPDATE||sct==SqlCommandType.DELETE){
if(InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())){
return;
}

MPBoundSqlmpBs=mpSh.mPBoundSql();
mpBs.sql(this.parserMulti(mpBs.sql(),(Object)null));
}

}

//碍于篇幅问题,下面省略的代码就是继承抽象类JsqlParserSupport解析SQL然后添加多租户id条件的,可以自行查看源码
......
}

接着我们来看看处理器TenantLineHandler,这是一个接口,需要我们提供自定义实现,指定多租户相关配置:

publicclassTenantDatabaseHandlerimplementsTenantLineHandler{
privatefinalSetignoreTables=newHashSet<>();

publicTenantDatabaseHandler(TenantPropertiesproperties){
//将配置文件配置的忽略表名同步大小写,适配不同写法
properties.getIgnoreTables().forEach(table->{
ignoreTables.add(table.toLowerCase());
ignoreTables.add(table.toUpperCase());
});
}

/**
*获取租户字段名
*

*默认字段名叫:tenant_id,我这里使用org_id * *@return租户字段名 */ @Override publicStringgetTenantIdColumn(){ return"org_id"; } @Override publicExpressiongetTenantId(){ //这里通过登录信息上下文返回租户id给多租户拦截器增强SQL使用 returnnewLongValue(RequestUserHolder.getCurrentUser().getOrgId()); } @Override publicbooleanignoreTable(StringtableName){ //忽略多租户的表 returnCollUtil.contains(ignoreTables,tableName); } }

配置属性如下:

@ConfigurationProperties(prefix="ptc.tenant")
@Data
publicclassTenantProperties{


/**
*全局控制是否开启多租户功能
*/
privateBooleanenable=Boolean.TRUE;

/**
*需要忽略多租户的表
*
*即默认所有表都开启多租户的功能,所以记得添加对应的tenant_id字段哟
*/
privateSetignoreTables=Collections.emptySet();
}

接下来注入拦截器插件即可:

@Bean
publicMybatisPlusInterceptormybatisPlusInterceptor(TenantPropertiesproperties){
MybatisPlusInterceptormybatisPlusInterceptor=newMybatisPlusInterceptor();
//必须保证多租户插件在分页插件之前,这个是MyBatis-plus的规定
if(properties.getEnable()){
mybatisPlusInterceptor.addInnerInterceptor(newTenantLineInnerInterceptor(newTenantDatabaseHandler(properties)));
}
//分页插件
mybatisPlusInterceptor.addInnerInterceptor(newPaginationInnerInterceptor());
returnmybatisPlusInterceptor;
}

使用示例如下:这里提供了一个常见的案例:用户和角色关联查询的SQL:getUserList()


selectu.*fromuseru
leftjoinuser_roleronu.id=r.user_id


andu.status=#{query.status}


andr.role_id=#{query.roleId}


and((u.namelikeconcat('%',#{query.keyword},'%'))or(u.mobilelikeconcat(#{query.keyword},'%')))


andu.entry_time>=#{query.startEntryTime}






groupbyu.id
orderbyu.iddesc

启动项目,先登录之后使用token掉接口执行下面代码逻辑:

publicPageResultgetList(UserQueryquery){
Pagepage=newPage<>(query.getPageNo(),query.getPageSize());
ListuserList=userDAO.getUserList(page,query);
ListuserDTOS=toUserDTOList(userList);
returnnewPageResult<>(userDTOS,page.getTotal(),page.getPages());
}

查看控制台发现:

[1658720355293990912][DEBUG][2023-05-1714:25:25.504][http-nio-16688-exec-1@23652]com.plasticene.textile.dao.UserDAO.getUserListdebug:==>Preparing:SELECTu.*FROMuseruLEFTJOINuser_rolerONu.id=r.user_idANDr.org_id=3WHEREu.org_id=3GROUPBYu.idORDERBYu.idDESCLIMIT?
[1658720355293990912][DEBUG][2023-05-1714:25:25.505][http-nio-16688-exec-1@23652]com.plasticene.textile.dao.UserDAO.getUserListdebug:==>Parameters:20(Long)

user表u加上u.org_id=3这个多租户过滤条件,user_role也同样加上了,说明多租户插件起作用了。

当然如果想忽略掉表user,我们只需要在配置文件如下配置即可:

ptc:
tenant:
ignore-tables:user

这样user表u就不会再加上u.org_id=3这个多租户过滤条件,但是这里有一个细节需要注意,由于user在MySQL中是关键字,所以我有时候为了规范书写SQL,会按照如下编写:

selectu.*from`user`u
leftjoinuser_roleronu.id=r.user_id

这时候你会发现上面配置的忽略表user不起作用,还是会加上u.org_id=3这个多租户过滤条件,跟源码才发现我们上面自定义的多租户处理器TenantLineHandler只对表名进行了大小写适配,然而这里SQL解析出来的表名是: user ,所以匹配不到配置不起作用。

当然我们有可能需要针对单一SQL语句不加多租户过滤条件,可以使用@InterceptorIgnore注解:

publicinterfaceUserDAOextendsBaseMapperX{

@InterceptorIgnore(tenantLine="true")
ListgetUserList(IPageuserPage,@Param("query")UserQueryquery);
}

这样调用getUserList()不再会加多租户过滤条件了。

通过上面我们知道了这个多租户插件其实就是通过解析SQL,然后进行拼接多租户id过滤条件来实现SQL增强从而做到数据隔离,解析SQL的框架叫:JSqlParser

官方文档:

https://github.com/JSQLParser/JSqlParser/wiki

Druid也可以解析SQL,我们都知道SQL语句会生成语法树,两者对SQL解析的孰强孰弱(特别是复杂SQL)不得而知,可以自行验证对比,我这里给出一个JSqlParser解析出错的情况,把上面的SQL语句user_role r 改为 user_role ur

selectu.*fromuseru
leftjoinuser_roleuronu.id=ur.user_id

按照上面一样调用执行getUserList(), 会报解析错误:

Causedby:com.baomidou.mybatisplus.core.exceptions.MybatisPlusException:Failedtoprocess,ErrorSQL:selectu.*fromuseru
leftjoinuser_roleuronu.id=ur.user_id
groupbyu.id
orderbyu.iddesc
atcom.baomidou.mybatisplus.core.toolkit.ExceptionUtils.mpe(ExceptionUtils.java:39)
atcom.baomidou.mybatisplus.extension.parser.JsqlParserSupport.parserSingle(JsqlParserSupport.java:52)
atcom.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor.beforeQuery(TenantLineInnerInterceptor.java:65)
atcom.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor.intercept(MybatisPlusInterceptor.java:78)
atorg.apache.ibatis.plugin.Plugin.invoke(Plugin.java:62)
atcom.sun.proxy.$Proxy178.query(UnknownSource)
atorg.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:151)
...101commonframesomitted
Causedby:net.sf.jsqlparser.parser.ParseException:Encounteredunexpectedtoken:"ur"
atline2,column29.

我在mybatis-plus的官方提了一个issue:https://github.com/baomidou/mybatis-plus/issues/5086,也得到官方维护者的迅速回应说是JSqlParser解析的问题,不是mybatis-plus的问题~~~,给出的建议就是把别名ur改成别的,或者升级到JSqlParser的最新版本。

4.总结

至此,我们对多租户系统数据隔离实现方案,架构设计,以及如何优雅实现全局操作数据隔离都讲完了,同时也对mybati-plus的多租户插件实现原理和源码流程套路进行了浅析,也对实际应用案例中进行了举证并阐述了相关细节点。

当然数据权限不止停留在租户(公司)层面上面,大多数系统的数据权限会按照业务组织架构角色来控制,数据权限其套路和根据角色判断菜单权限一回事。

由于数据权限通常与公司业务相关,比较个性化,每家公司业务组织架构不尽相同,所以实际开发项目的数据权限隔离还需要大家按实际需求进行修改,但总的来说我们可以模仿多租户隔离实现方式,比如说一个业务系统组织架构有公司(org_id),公司下有多个部门(dept_id),部门下有多个团队分组(team_id),团队下有多个人员(user_id)。

不同角色只能看到不同数据,部门经理只能看到自己部门的数据,小组长只能看到自己小组的数据,这些实现逻辑套路都可以模仿多租户插件的方式进行优雅实现,这也是我后面有时间想研究的,后续会再出一篇数据权限的实现方案总结。






审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 处理器
    +关注

    关注

    68

    文章

    19135

    浏览量

    228941
  • SQL
    SQL
    +关注

    关注

    1

    文章

    753

    浏览量

    44051
  • SaaS
    +关注

    关注

    1

    文章

    363

    浏览量

    36830
  • MYSQL数据库
    +关注

    关注

    0

    文章

    95

    浏览量

    9377
收藏 人收藏

    评论

    相关推荐

    什么是SaaS

    额外的成本,企业仍然需要购买、构建和维护自己独立的IT基础设备。SaaS为企业提供了另外一种选择。如今,企业可以自行安装注册,并通过互联网使用共享的基础设施服务。 促使客户采取SaaS解决方案
    发表于 07-13 09:23

    一种基于嵌入式系统隔离硬件设计

    本文在对已有的防火墙技术及物理隔离技术进行分析的基础上,提出了带缓冲区的双通道实时开关技术,通过该技术所设计出的网络隔离器能满足实时数据的传输,同时本文提出了一种物理
    发表于 04-25 07:58

    一种通道同步数据采集及压缩系统的设计方案  

    一种通道同步数据采集及压缩系统的设计方案。  
    发表于 04-28 06:13

    大神分享一种高速突发模式误码测试仪的FPGA实现方案

    大神分享一种高速突发模式误码测试仪的FPGA实现方案
    发表于 04-29 06:58

    大佬分享一种光谱可见光遥感图像压缩系统的设计方案

    大佬分享一种光谱可见光遥感图像压缩系统的设计方案
    发表于 06-02 06:39

    一种基于Android和Zigbee技术实现的家庭环境监控系统的设计方案

    齐全、运行稳定的家庭监控系统将具有非常大的市场竞争力。本文提出了一种基于Android和Zigbee技术实现的家庭环境监控系统的设计方案,该
    发表于 07-26 08:08

    一种基于STM8系列单片机的无线通信系统的设计与实现方案

    一种基于STM8系列单片机的无线通信系统的设计与实现方案
    发表于 12-17 06:23

    一种基于单片机的拖尾式模式流水灯系统仿真设计

    一种基于单片机的拖尾式模式流水灯系统仿真设计
    发表于 02-17 06:59

    奥威软件SaaS BI系统站式数据可视化解决方案

    SaaS BI 系统是 Software as a Service 商业智能的缩写,是一种基于云计算的商业智能解决方案。它允许用户通过互联网访问和分析
    发表于 08-01 10:21

    一种租户场景基于SLO的调度机制

    针对租户集群中无法保证作业服务水平目标( SLO)的问题,提出了一种租户场景基于SLO的调
    发表于 01-12 15:37 0次下载

    可信云租户隔离机制

    安全隔离机制的建设和管理过程,因此他们难以对云租户隔离机制的安全有效性建立信心.将透明性要求视为可信云租户隔离机制的
    发表于 01-13 10:21 0次下载

    Kubernetes租户集群的概念和常见的应用模式

    KaaS 租户方案通常与云服务提供商有关。在这种场景,业务平台的服务通过 Kubernetes 控制平面直接暴露给不同租户的用户。最终用
    的头像 发表于 05-15 16:13 1165次阅读
    Kubernetes<b class='flag-5'>多</b><b class='flag-5'>租户</b>集群的概念和常见的应用<b class='flag-5'>模式</b>

    基于SpringBoot实现租户架构:支持应用租户部署和管理

    租户架构是指在个应用中支持多个租户(Tenant)同时访问,每个租户拥有独立的资源和数据,并
    的头像 发表于 06-05 10:20 1343次阅读

    什么叫做saas,mybatis-plus优雅实现租户数据权限隔离方案

    租户问题,其是一种架构设计方式,就是在台或者组服务器上运行的SaaS
    发表于 07-27 10:34 3974次阅读

    SaaS 租户MES生产车间管理系统源码,生产排程,工艺流程,工序采集

    MES系统,即制造执行系统(Manufacturing Execution System),是一种用于管理和监控制造过程的软件系统。 它位于企业资源计划(ERP)
    的头像 发表于 08-14 17:31 267次阅读
    <b class='flag-5'>SaaS</b> <b class='flag-5'>多</b><b class='flag-5'>租户</b>MES生产车间管理<b class='flag-5'>系统</b>源码,生产排程,工艺流程,工序采集