大型SaaS系统的数据范围权限设计与实现!

芋道源码解析之数据权限 数据权限是一个比较常用的功能,芋道源码通过mybatis-plus自定义插件在SQL执行前拦截并解析到对应的表,根据数据权限规则对这些表追加where条件来实现数据权限控制,对于一些需要单独指定数据权限的业务方法,通过数据权限注解和结合线程上下文对加了注解的方法进行前置和后置的处理,把当前方法适用的数据权限规则传递给数据权限SQL解析器进行额外处理,使得数据权限规则既能全局生效又能局部调整。 阅读详情

点击上方“芋道源码”,选择“设为星标”

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 10:33 更新文章,每天掉亿点点头发...

源码精品专栏

 

来源:码农参上

4efe3781096e927d0314a3713ceddec0.jpeg


前言

多租户(Multi-Tenant)是SaaS中的一个重要概念,它是一种软件架构技术,在多个租户的环境下,共享同一套系统实例,并且租户之间的数据具有隔离性,也就是说一个租户不能去访问其他租户的数据。基于不同的隔离级别,通常具有下面三种实现方案:

  1. 每个租户使用独立DataBase,隔离级别高,性能好,但成本大

  2. 租户之间共享DataBase,使用独立的Schema

  3. 租户之间共享Schema,在表上添加租户字段,共享数据程度最高,隔离级别最低。

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

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

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

数据库设计

Mybatis-plus在第3层隔离级别上,提供了基于分页插件的多租户的解决方案,我们对此来进行介绍。在正式开始前,首先做好准备工作创建两张表,在基础字段后都添加租户字段tenant_id:

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL,
  `name` varchar(20) DEFAULT NULL,
  `phone` varchar(11) DEFAULT NULL,
  `address` varchar(64) DEFAULT NULL,
  `tenant_id` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
)
CREATE TABLE `dept` (
  `id` bigint(20) NOT NULL,
  `dept_name` varchar(64) DEFAULT NULL,
  `comment` varchar(128) DEFAULT NULL,
  `tenant_id` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
)

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

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

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

引入依赖

在项目中导入需要的依赖:

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.3.2</version>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>3.1</version>
</dependency>

实现

Mybatis-plus 配置类:

@EnableTransactionManagement(proxyTargetClass = true)
@Configuration
public class MybatisPlusConfig {
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();

        List<ISqlParser> sqlParserList=new ArrayList<>();
        TenantSqlParser tenantSqlParser=new TenantSqlParser();
        tenantSqlParser.setTenantHandler(new TenantHandler() {
            @Override
            public Expression getTenantId(boolean select) {
                String tenantId = "3";
                return new StringValue(tenantId);
            }

            @Override
            public String getTenantIdColumn() {
                return "tenant_id";
            }

            @Override
            public boolean doTableFilter(String tableName) {
                return false;
            }
        });

        sqlParserList.add(tenantSqlParser);
        paginationInterceptor.setSqlParserList(sqlParserList);
        return paginationInterceptor;
    }
}

这里主要实现的功能:

  • 创建SQL解析器集合

  • 创建租户SQL解析器

  • 设置租户处理器,具体处理租户逻辑

这里暂时把租户的id固定写成3,来进行测试。测试执行全表语句:

public List<User> getUserList() {
        return userMapper.selectList(new LambdaQueryWrapper<User>().isNotNull(User::getId));
        }

使用插件解析执行的SQL语句,可以看到自动在查询条件后加上了租户过滤条件:

8c18e62e151291781fe90e974abbfa63.png

那么在实际的项目中,怎么将租户信息传给租户处理器呢,根据情况我们可以从缓存或者请求头中获取,以从Request请求头获取为例:

@Override
public Expression getTenantId(boolean select) {
        ServletRequestAttributes attributes=(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String tenantId = request.getHeader("tenantId");
        return new StringValue(tenantId);
        }

前端在发起http请求时,在Header中加入tenantId字段,后端在处理器中获取后,设置为当前这次请求的租户过滤条件。

如果是基于请求头携带租户信息的情况,那么在使用中可能会遇到一个坑,如果当使用多线程的时候,新开启的异步线程并不会自动携带当前线程的Request请求。

@Override
public List<User> getUserListByFuture() {
        Callable getUser=()-> userMapper.selectList(new LambdaQueryWrapper<User>().isNotNull(User::getId));
        FutureTask<List<User>> future=new FutureTask<>(getUser);
        new Thread(future).start();
        try {
        return future.get();
        } catch (Exception e) {
        e.printStackTrace();
        }
        return null;
        }

执行上面的方法,可以看出是获取不到当前的Request请求的,因此无法获得租户id,会导致后续报错空指针异常:

fe917f1e61e57fcc2be9350cca49930e.png

修改的话也非常简单,开启RequestAttributes的子线程共享,修改上面的代码:

@Override
public List<User> getUserListByFuture() {
        ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        Callable getUser=()-> {
        RequestContextHolder.setRequestAttributes(sra, true);
        return userMapper.selectList(new LambdaQueryWrapper<User>().isNotNull(User::getId));
        };
        FutureTask<List<User>> future=new FutureTask<>(getUser);
        new Thread(future).start();
        try {
        return future.get();
        } catch (Exception e) {
        e.printStackTrace();
        }
        return null;
        }

这样修改后,在异步线程中也能正常的获取租户信息了。

那么,有的小伙伴可能要问了,在业务中并不是所有的查询都需要过滤租户条件啊,针对这种情况,有两种方式来进行处理。

1、如果整张表的所有SQL操作都不需要针对租户进行操作,那么就对表进行过滤,修改doTableFilter方法,添加表的名称:

@Override
public boolean doTableFilter(String tableName) {
        List<String> IGNORE_TENANT_TABLES= Arrays.asList("dept");
        return IGNORE_TENANT_TABLES.stream().anyMatch(e->e.equalsIgnoreCase(tableName));
        }

这样,在dept表的所有查询都不进行过滤:

14245f21126c073d3b9f17cac7826141.png

2、如果有一些特定的SQL语句不想被执行租户过滤,可以通过@SqlParser注解的形式开启,注意注解只能加在Mapper接口的方法上:

@SqlParser(filter = true)
@Select("select * from user where name =#{name}")
User selectUserByName(@Param(value="name") String name);

或在分页拦截器中指定需要过滤的方法:

@Bean
public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        paginationInterceptor.setSqlParserFilter(metaObject->{
        MappedStatement ms = SqlParserHelper.getMappedStatement(metaObject);
        // 对应Mapper、dao中的方法
        if("com.cn.tenant.dao.UserMapper.selectUserByPhone".equals(ms.getId())){
        return true;
        }
        return false;
        });
        ...
        }

上面这两种方式实现的功能相同,但是如果需要过滤的SQL语句很多,那么第二种方式配置起来会比较麻烦,因此建议通过注解的方式进行过滤。

除此之外,还有一个比较容易踩的坑就是在复制Bean时,不要复制租户id字段,否则会导致SQL语句报错:

public void createSnapshot(Long userId){
        User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getId, userId));
        UserSnapshot userSnapshot=new UserSnapshot();
        BeanUtil.copyProperties(user,userSnapshot);
        userSnapshotMapper.insert(userSnapshot);
        }

查看报错可以看出,本身Bean的租户字段不为空的情况下,SQL又自动添加一次租户查询条件,因此导致了报错:

563b096e19b1de4174ffb238d05ed0f3.png

我们可以修改复制Bean语句,手动忽略租户id字段,这里使用的是hutool的BeanUtil工具类,可以添加忽略字段。

BeanUtil.copyProperties(user,userSnapshot,"tenantId");

在忽略了租户id的拷贝后,查询可以正常执行。

最后,再来看一下对联表查询的支持,首先看一下包含子查询的SQL:

@Select("select * from user where id in (select id from user_snapshot)")
List<User> selectSnapshot();

查看执行结果,可以看见,在子查询的内部也自动添加的租户查询条件:

3600ba7714beb64e35eebeab8e778d43.png

再来看一下使用Join进行联表查询:

@Select("select u.* from user u left join user_snapshot us on u.id=us.id")
List<User> selectSnapshot();

同样,会在左右两张表上都添加租户的过滤条件:

7defbd8eca767120fc96b2e56d1eaec0.png

再看一下不使用Join的普通联表查询:

@Select("select u.* from user u ,user_snapshot us,dept d where u.id=us.id and d.id is not null")
List<User> selectSnapshot();
10a9a1fe167421f6c2761b0cd23953f9.png

查看执行结果,可以看见在这种情况下,只在FROM关键字后面的第一张表上添加了租户的过滤条件,因此如果使用这种查询方式,需要额外注意,用户需要手动在SQL语句中添加租户过滤。



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢:

6168cbb38e37ffc3d02ec69816dd6ff8.png

已在知识星球更新源码解析如下:

809729b9215a46958fc99c559277f054.jpeg

8c8e6d7f213e05ef443c32d78f93b9e2.jpeg

a452a43144e4096ae2931b0d7ece01f2.jpeg

99cd55bc3818f5d03b4c8ea9179737bf.jpeg

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
SAAS系统下基于RBAC的权限设计(2) - 数据结构设计 在SAAS管理端,前端使用Vue3,后端采用Java相关技术栈,以下所展示的数据结构仅作为参考,具体在项目实践中还需根据实际情况进行优化使用。以下数据结构已对后期的Client Site权限分配做了扩展准备,针对SAAS客户端,可做功能以及Api功能的权限隔离。 阅读详情

相关推荐

SaaS系统数据范围权限设计:从RBAC/ABAC到高性能实现

在构建多租户SaaS系统时,数据范围权限是保障数据安全与隔离的核心机制。其原理是在数据访问层嵌入精密的过滤逻辑,确保用户仅能操作其权限范围内的数据。这项技术的核心价值在于支撑规模化业务增长的同时,满足严格的合规性要求,是构建可信商业系统的基石。典型的应用场景包括企业内部的部门数据隔离、多租户SaaS平台中不同客户间的数据隔离,以及复杂的集团分级管控。本文聚焦于**数据范围权限**体系的设计与实现,深入探讨如何结合**RBAC(基于角色的访问控制)** 与**ABAC(基于属性的访问控制)** 模型,构建一套

weixin_29623317的博客 513

SaaS 应用之权限管理设计

SaaS 应用中常见的权限设计细节。

CherryXieのblog 1751

大型SaaS系统数据范围权限设计:从RBAC到动态数据域的实战解析

在软件即服务(SaaS)和企业级应用中,权限控制是保障数据安全与业务逻辑的核心基础。其原理在于通过系统化的规则,精确管理用户对功能和数据的访问能力。传统的基于角色的访问控制(RBAC)主要解决“能做什么”的功能权限问题,而现代复杂业务场景则进一步要求解决“能操作哪些数据”的数据权限挑战,这对于实现多租户数据隔离与内部精细化管控具有关键的技术价值。为此,业界演进出了结合RBAC与动态数据域的混合模型,通过规则引擎动态计算数据可见范围,从而灵活适应销售团队、区域管理等多样化的应用场景。本文深入探讨了如何设计并实

weixin_33785971的博客 314

芋道框架的权限控制机制

其中,注解@PreAuthorize("@ss.hasPermission('system:post:query')"),表明了,要想调用本方法,需要当前用户具有'system:post:query'权限。那么,@PreAuthorize("@ss.hasPermission('system:post:query')")这段代码是如何被执行的呢?可以看到,其中有一个方法hasPermission,正是上文中提到的注解中的方法。该方法的操作就很明显了,就是通过查询数据库,来判断当前的用户是否拥有对应的权限。

cluniquecui的专栏 3518

AI SaaS平台的用户权限体系设计:RBAC实战

本文旨在为AI SaaS平台的开发者提供一套完整的RBAC(基于角色的访问控制)设计方案和实施指南。我们将重点讨论如何在复杂的AI服务环境中实现精细化的权限管理。介绍RBAC核心概念分析RBAC在AI SaaS平台中的特殊需求展示RBAC系统的设计和实现提供实际应用案例和代码示例讨论未来发展趋势RBAC:基于角色的访问控制(Role-Based Access Control),一种通过角色来管理用户权限的安全模型权限:系统中定义的操作或资源访问能力角色。

架构师的AI之路,分享AI应用开发架构的学习与实践。 297

芋道管理后台,基于 Vue2 + Element UI 实现,支持 RBAC 动态权限、数据权限、SaaS 多租户

芋道管理后台,基于 Vue2 + Element UI 实现,支持 RBAC 动态权限、数据权限、SaaS 多租户、Flowable 工作流、三方登录、支付、短信、商城、CRM、ERP 等功能.zip

大型SaaS系统的数据范围权限设计与实现

大家好,我是宝哥!toB 的系统,除了普通的权限管理之外,往往还需要数据范围权限。本文介绍一种,简单的易实现的 Saas 多租户数据范围权限系统的简单设计与实现。权限的概述我们一般说权限的时候是在说「功能权限和数据权限」。功能权限指用「户登陆系统后能看到什么模块,能看到哪些页面」,而数据权限指的「是用户在某个模块里能看到几条数据,能看到哪些数据」。功能权限在企业系统中,通过配置用户的功能权限可以解...

程序员闪充宝 1086

OpenSceneGraph编译、安装、开发环境

详细描述WIndows 10下使用Visual Studio 2019编译OpenSceneGraph 3.6.5的操作流程

国产CAx(CAD/CAE/CAM)软件研发 8774

大型 SaaS 系统的数据范围权限该如何设计?

toB 的系统,除了普通的权限管理之外,往往还需要数据范围权限。本文介绍一种,简单的易实现的 Saas 多租户数据范围权限系统的简单设计与实现。权限的概述我们一般说权限的时候是在说「功能权限和数据权限」。功能权限指用「户登陆系统后能看到什么模块,能看到哪些页面」,而数据权限指的「是用户在某个模块里能看到几条数据,能看到哪些数据」。功能权限在企业系统中,通过配置用户的功能权限可以解决不同的人分管不同...

Java笔记虾 364

从零玩转显示屏:手把手教你用Arduino驱动SPI屏幕(附引脚定义图)

本文提供了一份详尽的Arduino驱动SPI屏幕的入门指南。从SPI接口的核心引脚定义(如DC、RESET)与RGB接口的区别讲起,手把手指导硬件连接与软件库配置,并最终通过一个天气显示器实战项目,帮助读者掌握驱动SPI屏幕、显示RGB色彩内容的核心技能。

weixin_29189987的博客 132

用STM32的SPI模式2搞定SSI绝对值编码器通信(附完整代码与避坑点)

本文详细解析了如何使用STM32的SPI模式2实现SSI绝对值编码器通信,包括硬件设计、SPI配置、数据处理算法及稳定性优化技巧。通过完整的代码示例和避坑指南,帮助工程师解决时序匹配、数据解析等常见问题,提升工业自动化系统中的编码器通信可靠性。

weixin_27252001的博客 269

大型Saas系统的权限体系设计(二)

比较好的解决办法是把上下级关系建立在部门岗位基础上,由岗位形成上下级关系,而个人可以设定多个岗位,则人与人之间的上下级关系自然而然建立,同时又避免了因为上级岗位变动带来的不必要的大面积变更。设想下,一个成功的Saas系统,用户少则几万,多则千万,如果这么多的用户的权限管理都归于平台管理员一身,本身就是巨大的成本。提到2B的Saas系统的多层次权限体系设计的难题,即平台、平台的客户、客户的客户,乃至客户的客户的客户如何授权,这个可以通过“权限-角色-岗位”三级结构来实现。答案是肯定的,而且必须下放。

顽刀3j%es(3Qm$d1 2281

第一章 Cheat Enginee(CE)使用指南

Cheat Engine核心功能详细说明(以植物大战僵尸为例) 教你修改单机游戏。

m0_74545731的博客 1万+

大型 SaaS 系统的数据权限就应该这么设计

toB 的系统,除了普通的权限管理之外,往往还需要数据范围权限。本文介绍一种,简单的易实现的 Saas 多租户数据范围权限系统的简单设计与实现。权限的概述我们一般说权限的时候是在说「功能权限和数据权限」。功能权限指用「户登陆系统后能看到什么模块,能看到哪些页面」,而数据权限指的「是用户在某个模块里能看到几条数据,能看到哪些数据」。功能权限在企业系统中,通过配置用户的功能权限可以解决不同的人分管不同...

程序员闪充宝 609

OceanBase迁移平台OMS企业版部署

OceanBase迁移平台OMS企业版部署摘要: 本文档详细介绍了OceanBase迁移平台OMS企业版的部署流程,包含环境准备、OAT部署管理、MetaDB部署等关键步骤。

Sebastien23的博客 523

骑砍Ⅱ霸主MOD开发(16)-FaceKey&BannerKey

摘要:本文介绍了FaceKey人物模型和BannerKey旗帜模型的生成流程。FaceKey通过序列化参数生成人物模型,包括头部(脸部、嘴、睫毛、眼睛)、眉毛、头发、身体和躯干等组件,每个部分通过特定标签和骨骼绑定实现。BannerKey则通过解析参数生成旗帜模型,包括模型、贴图和颜色等元素。两种模型都采用参数化设计和XML配置文件实现模型生成与定制。

骑砍战团&霸主MOD开发 2289

大型Saas系统的权限体系设计(一)

在2B系统开发中,权限体系设计是绕不开的问题。最简单的当然是RBAC模型,只要通过用户、角色、权限几个有限的概念,就可以建立起一套基本可用的权限体系。再复杂一点,可以增加角色的层级概念,使得角色的配置更高效。但现实工作中,权限控制的需求往往更复杂,不仅涉及到功能授权,也涉及到数据授权,有些系统甚至涉及到更细的数据颗粒度授权,这个时候RBAC模型就显示出诸多的不足。如何设计一套简单高效的权限体系,使得能够满足各种从简单到复杂的场景的应用,是产品经理和系统架构师需要用心的基础工作。

顽刀3j%es(3Qm$d1 2290

国社科基金申报书“选题与说明依据”用AI怎么写?借助Gemini 3 Pro一次理清研究演进逻辑

研究进展部分要明确指出已有研究的不足与局限,比如方法不足、数据或材料受限、研究视角单一等等局限,之后要找准研究切入点,自然引申出自己的研究问题。为提升课题申报书的质量,学术史梳理要遵循述评结合、中外结合、新老结合的原则,不仅“述”,更要“评”,兼顾国内研究与国际学术动态,统筹经典研究与最新成果。它是对选题的整体概述,需阐明研究的具体问题、视角以及核心概念。2)存在问题或争议:结合已有研究或实践现状,指出该领域在理论建构、测量工具、实践应用或研究结论方面仍存在的不足、空白或争议,明确研究的必要性;

智能AI让学术科研更容易 943

今天来聊一聊大型SaaS系统的数据范围权限设计与实现.

如果是基于请求头携带租户信息的情况,那么在使用中可能会遇到一个坑,如果当使用多线程的时候,新开启的异步线程并不会自动携带当前线程的Request请求。上面这两种方式实现的功能相同,但是如果需要过滤的SQL语句很多,那么第二种方式配置起来会比较麻烦,因此建议通过注解的方式进行过滤。我们可以修改复制Bean语句,手动忽略租户id字段,这里使用的是hutool的BeanUtil工具类,可以添加忽略字段。那么,有的小伙伴可能要问了,在业务中并不是所有的查询都需要过滤租户条件啊,针对这种情况,有两种方式来进行处理。

2301_76936922的博客 790

Android日志实战——车机日志获取(十五)

这个项目中的 Log 管理系统,其实只是将系统原生 Log 进行输出并导出,并没有太复杂的操作。还有一种 Log 管理系统需要对 Android 原生 Log 系统进行重写,例如我们使用 Log.d(tag, msg) 打印 Log,需要我们根据 tag 信息将不同模块的 Log 数据写入不同的本地文件下,这样可以方便不同模块查看自己的 Log,追踪异常问题。

小旭的专栏 3780

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

大型 SaaS 平台产品架构设计

当我们去搜索“架构”,可以得到很多的架构图片,比如组织架构、业务架构、数据架构、技术架构、安全架构、产品架构、部署架构等。什么是架构,通常大家说架构一般指软件架构,架构是指软件的基础结构,创造这些基础结构的准则,以及对这些结构的描述。在这个定义基础上,我们可以简单理解为架构往往是对事物主体的结构性描述。产品架构是对产品的一种结构性描述。一般可以包括前端系统、业务管理、运营...

架构师小秘圈 528

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

上一篇: 聊聊大厂都在用的 JavaAgent
下一篇: 从3s到25ms!看看人家的接口优化技巧,确实很优雅!!
公众号-芋道源码
博客等级 码龄10年 1634粉丝 377原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值