数据权限的设计与实现系列13——前端筛选器组件Everright-filter集成多控制维度实现

数据权限多维度实现

上面的所有工作,实际都是基于业务实体属性这一数据权限控制维度展开的。
接下来,我们来设计与实现多维度,主要是用户组(即角色)、组织机构和用户。

业务需求分析

用户控制维度

业务场景:销售员只能查看自己的销售订单
实现方式:销售订单的创建人属性=当前用户标识

组织机构控制维度

业务场景:部门内备品备件库存部门内所有成员可见
实现方式:备品备件的归属部门=当前用户所在部门

用户组控制维度

用户组这个数据权限控制维度如果单独使用,本质上与基于RBAC模型的功能权限的控制方式重叠了。因此通常是结合其他控制维度来进行多维度的复杂控制。

业务场景1:销售部门经理可查看本部门所有销售订单
实现方式:
销售订单的创建部门 = 当前用户所在部门
且 当前用户拥有的用户组 包含 销售部门经理


业务场景2:分管销售的副总裁只关注销售额大于100万的销售订单
实现方式:
销售订单的金额 > 100 万
且 当前用户拥有的用户组 包含 分管销售的副总裁

业务场景3:某个人挂靠在A部门下,但是需要处理同级的多个部门如B、C的数据,或子部门A1、A2的数据。
实现方式:
业务实体对象的归属部门 包含在 指定的若干个部门列表中
且 当前用户拥有的用户组 包含 指定的某个具体的用户组

还需要额外说明的一点是实际业务需求情况会复杂多变,非必要应避免引入新的数据权限控制维度,而是基于现有的维度做一些变通与转换。例如一个销售经理带两个销售员,要求销售经理可以查看自己和下属的销售订单,这时候不建议引入新的数据权限控制维度,如销售员与销售经理这种人员的上下级关系。优先考虑是否能够转换成现有控制维度来实现,例如,将销售经理和他的团队放到一个同一部门(可能是虚拟的组织机构)下,通过组织机构这个权限控制维度来实现。

技术实现

基于上述业务场景分析,我们需要做以下工作:

  1. 筛选器调用后端服务获取实体属性列表时,自动添加一个 当前用户组 属性作为筛选条件
  2. 约定自定义变量的含义与占位符,并在数据权限拦截处理的SQL片段中将其替换为运行时获取到的真实值
    • 当前用户 {@CurrentUser@}
    • 当前用户部门 {@CurrentDepartment@}
    • 当前用户组 {@CurrentUserGroup@}
  3. 复用数据筛选器的级联框展现控件类型,用来处理组织机构选择和用户组选择

‍‍

组织机构维度控制

业务实体对象的归属部门,可能有两种操作,一种是等于当前用户部门;另一种是在组织结构树中选择多个,结合用户组来控制。为了统一操作,可以将组织机构设置为级联框,然后将当前用户部门虚拟为根节点,对应值为{@CurrentDepartment@}。

这里有个小问题,对于数据字典和级联框,都需要调用后端服务获取数据源,但二者共享getConditions这个方法,并且这个方法只有一个参数,没法区分到底是数据字典还是级联框。
考虑到使用级联框仅有组织机构和用户组两类特定的数据,因此可以固化其编码,优先判断,不匹配时则就是数据字典了。

技术验证

同样,先拿先前的demo页面进行功能验证和调试。
筛选条件中增加部门,同步新增一个对应的操作,如下:

调整获取数据的方法getConditions,如下:

运行,整体效果出来了,如下:

不过这里有个关键问题,当选择了父级后,所有子级会自动清空,这样的话我们去判断是否包含就麻烦了,不仅需要判断集合中的值,而且还要判断集合中值的所有子部门,包括子部门的下级……

问题出在筛选器组件封装CASCADE的时候没有单独设置checkStrictly属性,而是复用了multiple属性,源码如下:

在进行数据权限控制的场景下,大多数情况下也是符合预期的,一般情况下,会在某个水平层级来控制数据权限,很少会跨越多个不同层级,因为从逻辑上就比较混乱,不利于运维。因此接受筛选器当前现状,只进行直接的集合包含关系的判定,不考虑子级。

正式实现

技术验证通过,我们将其迁移到正式环境下。
首先,前后端交互的视图对象EntityModelPropertyForFilterVO,新增两个属性,集合类型与是否多选属性。

其次,在筛选器组件调用的后端服务的地方,增加对组织机构相关属性的处理,如下:

此外,平台组织机构控制器中增加了将组织机构数据转换为级联框需要的数据结构,使用了递归,如下:

 /**
     * 获取级联框数据
     *
     * @return
     */
    @GetMapping("/cascader")
    @PreAuthorize("hasPermission(null,'system:organization:query')")
    public ResponseEntity<Result> cascader() {
        QueryWrapper<Organization> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(Organization::getStatus, StatusEnum.NORMAL.toString());
        // 附加按照排序号排序
        queryWrapper.orderByAsc(TableFieldConstant.DEFAULT_SORT_FILED);
        List<Organization> list = organizationService.list(queryWrapper);
        Organization rootOrganization = list.stream().filter(x -> x.getOrganization().equals(TreeDefaultConstant.DEFAULT_TREE_ROOT_PARENT_ID))
                .findFirst().get();

        CascaderItemVO root = convert2CascadeVO(rootOrganization);
        root.setChildren(convertToCascaderData(rootOrganization.getId(), list));
        return ResultUtil.success(root);
    }

    /**
     * 转换为级联数据
     *
     * @param parentId         父级标识
     * @param organizationList 组织机构列表
     * @return 列表<cascader项目vo>
     */
    private List<CascaderItemVO> convertToCascaderData(String parentId, List<Organization> organizationList) {
        List<CascaderItemVO> cascaderData = new ArrayList<>();
        List<Organization> subOrganizationList = organizationList.stream().filter(x -> x.getOrganization().equals(parentId))
                .collect(Collectors.toList());
        for (Organization organization : subOrganizationList) {
            CascaderItemVO cascaderItem = convert2CascadeVO(organization);
            List<CascaderItemVO> children = convertToCascaderData(organization.getId(), organizationList);
            cascaderItem.setChildren(children);
            cascaderData.add(cascaderItem);
        }
        if (CollectionUtils.isNotEmpty(cascaderData)) {
            return cascaderData;
        } else {
            return null;
        }
    }
    /**
     * 转换为级联框视图对象
     */
    private CascaderItemVO convert2CascadeVO(Organization entity) {
        CascaderItemVO vo = new CascaderItemVO();
        vo.setValue(entity.getId());
        vo.setLabel(entity.getName());
        return vo;
    }

再次,前端发起调用上面的后端服务,以及拿到数据后的处理,如下:

运行,效果如下:

测试后续步骤,如下:

发现in没起作用,检查后是因为IN应该大写,调整后如下:

in语句能解析了,不过最后生成的SQL片段不对,多了引号和中括号,调整原来的代码,如下:

调整后,生成了正确的sql,如下:
(organizaiton IN (‘1186911361171308545’,‘1186911884368789506’))

增加当前部门

前面在组织机构功能调试正常的基础上,我们增加先前梳理的特殊控制:当前用户所属部门。
通过虚拟一个当前用户部门的节点,与平台的组织机构树并列,对应值为{@CurrentDepartment@},该部分工作在前端完成,如下:

实现效果如下:

注意,这里系统没有进行严格控制,从操作上用户可以混选虚拟的“当前用户部门”和组织机构中的数据。因为是基于筛选器组件自身功能扩展存在局限性,进行二选一控制实现起来比较麻烦,不过这点影响很小。主要在于进行数据权限配置的是系统管理员而不是业务用户,因此从操作上可以遵循二选一的模式,并不需要非得由系统来严格控制。

继续后续测试,生成规则正常,转换SQL片段也正常,如下:

虽然在用户当前部门模式下使用等于更合理,不过出于简洁的考虑,复用集合下的in也没什么问题,执行结果是一样的,至于性能差异,可能有一点点。

然后我们修改Mybatisplus数据权限处理器,将自己约定的{@CurrentDepartment@}占位符,替换为运行环境下的用户部门,如下:


查看后端最终执行的SQL语句,可以看到,正确附加了当前用户部门:
SELECT COUNT(*) AS total FROM cfg_template WHERE delete_flag = ‘NO’ AND (organizaiton IN (‘1186911361171308545’))

用户组维度控制

用户组维度的控制与组织机构维度控制高度相似,不展开赘述,只说差异点,主要有以下几点:

  1. 需要在左侧,筛选条件中增加一个虚拟的条件,当前用户拥有的用户组
  2. 右侧用户组是单选而不是多选
  3. 左侧用户当前拥有的用户组是一个集合,需要包含右侧选择的用户组

针对这几点差异来说说设计与实现。

前端在调用后端服务,拿到当前实体的模型属性后,在最后追加虚拟的条件,如下:

增加新的操作集合Contain,如下:

后端增加对包含操作符CT的处理,如下:


效果如下:

为用户组实现级联框数据转换,效果如下:


接下来是占位符的替换工作了,略复杂一些,但是也好实现。
无非是将当前用户拥有的用户组列表,转换成sql语句的in结构后替换掉占位符,如下:


最终(‘99’ IN (‘{@CurrentUserGroup@}’))替换后成为了标准的sql片段,如下:
AND (‘99’ IN (‘99’, ‘1’))

用户维度控制

用户控制维度相比前面两个要简单得多。当实体模型属性为用户时,比如平台预设的创建人和更新人属性,操作集合可以只设置一个等于,然后值为我们预设的特殊占位符{@CurrentUser@},在数据权限处理器中,获取当前运行的用户后,将占位符替换为真实的值。

整体思路就是上面说的,接下来简单列列要做的工作。

首先,获取实体属性转换为筛选器查询条件环节,增加用户属性的处理:

对应前端操作集合为:

在getConditions中增加相应的逻辑分支,如下:

这里虚拟了一个下拉列表选项,注意写法,调用筛选器的方法,是一个对象,这个对象需要有一个data属性,且该属性是一个数组。

最后,替换占位符,如下:

运行效果如下:

实际测试,查看控制台输出的sql,符合预期。

此外,这里的值设定,使用的是下拉列表的方式,主要还是考虑复用组件自身的功能。还考虑过一种实现方案,即设定一个新的操作类型,就叫“等于当前用户”,然后值控件设置为none(不显示)。但是这样新增的这个操作类型,与先前定义的等于、大于、包含等就不是同一维度的分类了,逻辑上显得混乱,因此未使用该方案。

总结

至此,我们完成了一二三开发平台的数据权限的设计与实现,通过配置,可以灵活的进行多维度的数据权限控制,包括用户、组织机构、用户组以及业务实体属性,控制对象为各个业务实体,无耦合,无侵入。

开源平台资料

平台名称:一二三开发平台
简介: 企业级通用开发平台
设计资料:[****专栏]
开源地址:[Gitee]
开源协议:MIT
如果您在阅读本文时获得了帮助或受到了启发,希望您能够喜欢并收藏这篇文章,为它点赞~
请在评论区与我分享您的想法和心得,一起交流学习,不断进步,遇见更加优秀的自己!

上一篇:【设计模式系列】装饰器模式


下一篇:各类名词term解释....