专业的编程技术博客社区

网站首页 > 博客文章 正文

快速开发平台 ruoyi-vue-pro(6)数据权限功能设计分析和实测

baijin 2024-09-12 11:10:54 博客文章 10 ℃ 0 评论

我们在之前已经分析过该项目的搭建和一部分功能实测体验

该项目历史文章:

快速开发平台 ruoyi-vue-pro(1)- 项目搭建和功能体验

快速开发平台 ruoyi-vue-pro(2)工作流引擎模块

快速开发平台 ruoyi-vue-pro (3) - 账号授权体系实测分析

快速开发平台 ruoyi-vue-pro(4)- 商城移动端功能实测

快速开发平台 ruoyi-vue-pro(5)支付中心功能设计分析和实测


今天我们来看一下项目中的数据权限功能是怎么用的,以及它的技术设计思路

  • 数据权限功能测试

首先我们可以在角色管理中针对角色进行数据权限的维护,可以看到,数据权限是基于 角色+部门 来实现的,权限范围也是围绕着用户自身和所在的部门来设计的,我接下来定义两个角色来测试一下。



增加了两个角色,一个普通的,只能看自己的数据;另一个是小组长,可以看本部门以及下级部门的数据

我们把两个角色都开通“用户管理”这个功能菜单,现在我们试试用test1普通员工的权限登录一下

可以看到在这个菜单里,test1普通员工的账号因为数据权限是仅限自己的数据,所以只能看到自己。

然后使用test2账号进行登录,发现如数据权限范围所设定的一样,他可以看到研发部门下的所有数据。

其余的数据权限范围我就不逐一去测试了,其实最主要用的就是这种场景。

  • 技术原理分析

首先我们找到数据权限模块的底层工程,进入它的配置类从而开始分析

其主要定义了3个Bean。

  1. DataPermissionRuleFactory
    数据权限规则工厂,用来将项目中的数据权限规则的操作做成对外服务的统一出口,可以看到这个工厂接口定义了2个主要方法,即“获得所有数据权限规则数组”,“获得指定 Mapper 的数据权限规则数组”。这里的Mapper其实就是mybatis的dao接口,数据权限最终的实现是要基于数据库查询的操作类,给它的sql注入数据权限过滤的片段。

目前项目中只实现了一个默认的DataPermissionRuleFactoryImpl,其实可以理解成就是这个工厂就是一个util类而已,它这里的工厂模式设计是便于后面的扩展,比如具体的Rule配置可以直接从数据库获取,这样只需要新写一个FactoryImpl即可。

  1. DataPermissionDatabaseInterceptor

这个是数据权限拦截器,可以从上面的图中看到,具体这个Bean内部其实就是增加了一个Mybatis的拦截器

这个拦截器实现了Mybatis plus的 sql解析支持类和拦截接口,另外可以看到,它需要将上面定义的DataPermissionRuleFactory注入进来,其实也就是DataPermissionRuleFactoryImpl

  1. DataPermissionAnnotationAdvisor

基于spring aop的拦截切面设置,可以看到它这里主要是拦截 @DataPermission 的类和方法,最后合并起来形成一个切面


上面分析了数据权限模块的第一个配置类,它其实还有第二个

这个是基于部门的数据权限相关的配置,它主要是将项目工程里所有的DeptDataPermissionRuleCustomizer实现类抓取到,然后将权限API操作类注入到一个实例化的rule,最后将rule进行自定义操作方法调用后返回。

说简单点,就是把rule对象丢到项目里每个自定义的DeptDataPermissionRuleCustomizer里去customize

搜索一下,项目里的system工程,就自定义了一个,放在数据权限实现里去理解,它这里就是配置了 system_users 表需要按照dept_id去实现部门权限控制;而system_dept 表给定了字段,那就直接按照id来控制

可以看出来,整个数据权限控制细化到表和字段级别的配置,是直接在代码里控制的!而界面上能操作的只是给角色定义好 部门权限范围


配置的部分看完了,接下来看看核心拦截过程!

数据权限拦截的核心主要集中在DataPermissionDatabaseInterceptor

可以大致分为 before builder process 三个部分,其中before就是在查询或者预处理阶段针对规则引擎进行初始化等操作;而builder是将配置好的具体数据权限规则转化为具体按表按字段的sql片段,process就是具体的执行过程,其内部也是调用到builder部分。

整体可以理解为是一个sql拆解重新拼装的过程,首先利用mybatis的sql解析插件进行sql拆解,然后在条件拼装过程中加载到数据权限的配置,然后进行sql条件重写。我们可以重点看下这个方法

这里先是从上下文中拿到配置的rules(在之前的before 里初始化的),然后从table对应的Rule里去获取表达式getExpression,而具体的Expression表达式是我们之前看到的Rule的实现类DeptDataPermissionRule里定义的

public Expression getExpression(String tableName, Alias tableAlias) {
        // 只有有登陆用户的情况下,才进行数据权限的处理
        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
        if (loginUser == null) {
            return null;
        }
        // 只有管理员类型的用户,才进行数据权限的处理
        if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) {
            return null;
        }

        // 获得数据权限
        DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class);
        // 从上下文中拿不到,则调用逻辑进行获取
        if (deptDataPermission == null) {
            deptDataPermission = permissionApi.getDeptDataPermission(loginUser.getId());
            if (deptDataPermission == null) {
                log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser));
                throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限",
                        loginUser.getId(), tableName, tableAlias.getName()));
            }
            // 添加到上下文中,避免重复计算
            loginUser.setContext(CONTEXT_KEY, deptDataPermission);
        }

        // 情况一,如果是 ALL 可查看全部,则无需拼接条件
        if (deptDataPermission.getAll()) {
            return null;
        }

        // 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限
        if (CollUtil.isEmpty(deptDataPermission.getDeptIds())
            && Boolean.FALSE.equals(deptDataPermission.getSelf())) {
            return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空
        }

        // 情况三,拼接 Dept 和 User 的条件,最后组合
        Expression deptExpression = buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds());
        Expression userExpression = buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId());
        if (deptExpression == null && userExpression == null) {
            // TODO 芋艿:获得不到条件的时候,暂时不抛出异常,而是不返回数据
            log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]",
                    JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission));
//            throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空",
//                    loginUser.getId(), tableName, tableAlias.getName()));
            return EXPRESSION_NULL;
        }
        if (deptExpression == null) {
            return userExpression;
        }
        if (userExpression == null) {
            return deptExpression;
        }
        // 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE (dept_id IN ? OR user_id = ?)
        return new Parenthesis(new OrExpression(deptExpression, userExpression));
    }

可以看出来,在获取表达式过程中,规则里加载了用户登录信息、权限配置,最终形成了一个实际的sql过滤条件。

最后的最后,就是把过滤条件组装好后重写SQL,切到Mybatis执行SQL的前夕,让它执行重写后的叠加了数据权限的SQL。


至于SQL解析的细节,我们就不赘述了,可以去参考下sql解析插件的机制。


  • 总结

我们本篇小小测试了一下项目中的数据权限功能。

1、系统支持在角色中配置 用户&部门级别的数据权限范围,然后系统的控制器中,追加 @DataPermission 注解(其实不加也可以,默认就是打开的,它这里是反选逻辑,如果加了并且参数里进行了禁用,则就表示不拦截),就能实现数据的拦截权限

2、设计上还不太完善的点:

如果需要去细化控制相同角色在不同功能上的数据权限是做不到的

数据权限控制涉及的表以及字段需要在代码里去配置

3、总体来说技术设计上还是比较优雅的,可扩展性也比较强

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表