SpringCloud微服务实战——搭建企业级开发框架(二十八):扩展MybatisPlus插件DataPermissionInterceptor实现数据权限控制

一套完整的系统权限需要支持功能权限和数据权限,前面介绍了系统通过RBAC的权限模型来实现功能的权限控制,这里我们来介绍,通过扩展Mybatis-Plus的插件DataPermissionInterceptor实现数据权限控制。

简单介绍一下,所谓功能权限,顾名思义是指用户在系统中拥有对哪些功能操作的权限控制,而数据权限是指用户在系统中能够访问哪些数据的权限控制,数据权限又分为行级数据权限和列级数据权限。

数据权限基本概念:

  • 行级数据权限:以表结构为描述对象,一个用户拥有对哪些数据的权限,表示为对数据库某个表整行的数据拥有权限,例如按部门区分,某一行数据属于某个部门,某个用户只对此部门的数据拥有权限,那么该用户拥有此行的数据权限。
  • 列级数据权限:以表结构为描述对象,一个用户可能只对某个表中的部分字段拥有权限,例如表中银行卡、手机号等重要信息只有高级用户能够查询,而一些基本信息,普通用户就可以查询,不同的用户角色拥有的数据权限不一样。

实现方式:

  • 行级数据权限:
    对行级数据权限进行细分,以角色为标识的数据权限,分为:
    1、只能查看本人数据;
    2、只能查看本部门数据;
    3、只能查看本部门及子部门数据;
    4、可以查看所有部门数据;
    以用户为标识的数据权限,分为:
    5、同一功能角色权限拥有不同部门的数据权限;
    6、不同角色权限拥有不同部门的数据权限。
    第1/2/3/4类的实现方式需要在角色列表对角色进行数据权限配置,针对某一接口该角色拥有哪种数据权限。
    第5类的实现方式,需要在用户列表进行配置,给用户分配多个不同部门。
    第6类的实现方式比较复杂,目前有市面上的大多数解决方案是:
    1、在登录时,判断用户是否拥有多个部门,如果存在,那么首先让用户选择其所在的部门,登录后只对选择的部门权限进行操作;
    2、针对不同部门创建不同的用户及角色,登录时,选择对应的账号进行登录。
    个人因秉承复杂的系统简单化,尽量用低耦合的方式实现复杂功能的理念,更倾向于第二种方式,原因是:
1、系统实现方面减少复杂度,越复杂的判断,越容易出问题,不仅仅在开发过程中,还在于后续系统的扩展和更新过程中。
2、对于工作量方面的取舍,一个人拥有多个部门不同权限的方式属于常用功能,但是并不普遍,也就是说在一家企业中,同一个用户即是业务部门经理,又是财务部门经理的情况并不普遍,更多的是专人专职。这里要和第5类做好区分,比如你是业务部门经理可能会管理多个部门,这种属于权限一致,只是拥有多个部门权限,这属于第5类。再比如一个总经理,可能会看到所有的业务、财务数据这属于第4类。
所以这里不会采取用户登录后选择部门的方式来判断数据权限。
  • 列级数据权限:
    列级数据权限的实现主要是针对某个角色能够看到哪些字段,不存在针对某个用户给他特定字段的情况,这种情况单独建立一个角色即可,尽量采用类RBAC的方式来实现,不要使用户直接和数据权限关联。列级数据权限除了要考虑后台取数据的问题,还要考虑到在界面上展示时,如果是一个表格,那么没有权限的列需要根据数据权限来判断是否展示。这里在配置界面就要考虑,在角色配置时,需要分为行级数据权限和列级数据权限进行不同的配置:行级数据权限应该配置需要数据权限控制的接口,数据权限的类型(上面提到的1234);列级数据权限除了需要配置上面提到的之外,还需要配置可以访问的字段或者排除访问的字段。

SpringCloud微服务实战——搭建企业级开发框架(二十八):扩展MybatisPlus插件DataPermissionInterceptor实现数据权限控制
在资源管理配置资源关联接口的数据权限规则(t_sys_data_permission_role),通过RBAC的方式用角色和用户关联,在用户管理配置用户同角色的多个部门数据权限,用户直接和部门关联(t_sys_data_permission_user)。系统数据权限管理功能设计如下所示:

SpringCloud微服务实战——搭建企业级开发框架(二十八):扩展MybatisPlus插件DataPermissionInterceptor实现数据权限控制
数据权限表设计:
CREATE TABLE t_sys_data_permission_user  (
  id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  tenant_id bigint(20) NOT NULL DEFAULT 0 COMMENT '租户id',
  user_id bigint(20) NOT NULL COMMENT '用户id',
  organization_id bigint(20) NOT NULL COMMENT '机构id',
  status tinyint(2) NULL DEFAULT 1 COMMENT '状态 0禁用,1 启用,',
  create_time datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
  creator bigint(20) NULL DEFAULT NULL COMMENT '创建者',
  update_time datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
  operator bigint(20) NULL DEFAULT NULL COMMENT '更新者',
  del_flag tinyint(2) NULL DEFAULT 0 COMMENT '1:删除 0:不删除',
  PRIMARY KEY (id) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
CREATE TABLE t_sys_data_permission_role  (
  id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  tenant_id bigint(20) NOT NULL DEFAULT 0 COMMENT '租户id',
  resource_id bigint(20) NOT NULL DEFAULT 0 COMMENT '功能权限id',
  data_name varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '数据权限名称',
  data_mapper_function varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '数据权限对应的mapper方法全路径',
  data_table_name varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '需要做数据权限主表',
  data_table_alias varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '需要做数据权限表的别名',
  data_column_exclude varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '数据权限需要排除的字段',
  data_column_include varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '数据权限需要保留的字段',
  inner_table_name varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '数据权限表,默认t_sys_organization',
  inner_table_alias varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '数据权限表的别名,默认organization',
  data_permission_type varchar(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '1' COMMENT '数据权限类型:1只能查看本人 2只能查看本部门 3只能查看本部门及子部门 4可以查看所有数据',
  custom_expression varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '自定义数据权限(增加 where条件)',
  status tinyint(2) NOT NULL DEFAULT 1 COMMENT '状态 0禁用,1 启用,',
  comments varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
  create_time datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
  creator bigint(20) NULL DEFAULT NULL COMMENT '创建者',
  update_time datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
  operator bigint(20) NULL DEFAULT NULL COMMENT '更新者',
  del_flag tinyint(2) NULL DEFAULT 0 COMMENT '1:删除 0:不删除',
  PRIMARY KEY (id) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '数据权限配置表' ROW_FORMAT = DYNAMIC;

数据权限缓存(Redis)设计:
  • Redis Key:
    多租户模式:auth:tenant:data:permission:0(租户):mapper_Mapper全路径_type_数据权限类型
    普通模式:auth:data:permission:mapper_Mapper全路径_type_数据权限类型
  • Redis Value:存放角色分配的DataPermissionEntity配置
    数据权限插件在组装SQL时,首先通过前缀匹配查询mapper的statementId是否在缓存中,如果存在,那么取出当前用户的数据权限类型,组装好带有数据权限类型的DataPermission缓存Key,从缓存中取出数据权限配置。
    在设计角色时,除了需要给角色设置功能权限之外,还要设置数据权限类型,角色的数据权限类型只能单选(1只能查看本人 2只能查看本部门 3只能查看本部门及子部门 4可以查看所有数据5自定义)
代码实现:
  • 因DataPermissionInterceptor默认不支持修改selectItems,导致无法做到列级别的数据权限,所以这里自定义扩展DataPermissionInterceptor,使其支持列级权限扩展
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class GitEggDataPermissionInterceptor extends DataPermissionInterceptor {

    private GitEggDataPermissionHandler dataPermissionHandler;

    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        if (!InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
            PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
            mpBs.sql(this.parserSingle(mpBs.sql(), ms.getId()));
        }
    }

    protected void processSelect(Select select, int index, String sql, Object obj) {
        SelectBody selectBody = select.getSelectBody();
        if (selectBody instanceof PlainSelect) {
            PlainSelect plainSelect = (PlainSelect)selectBody;
            this.processDataPermission(plainSelect, (String)obj);
        } else if (selectBody instanceof SetOperationList) {
            SetOperationList setOperationList = (SetOperationList)selectBody;
            List<selectbody> selectBodyList = setOperationList.getSelects();
            selectBodyList.forEach((s) -> {
                PlainSelect plainSelect = (PlainSelect)s;
                this.processDataPermission(plainSelect, (String)obj);
            });
        }

    }

    protected void processDataPermission(PlainSelect plainSelect, String whereSegment) {
        this.dataPermissionHandler.processDataPermission(plainSelect, whereSegment);
    }

}
</selectbody>
  • 自定义实现DataPermissionHandler数据权限控制
@Component
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class GitEggDataPermissionHandler implements DataPermissionHandler {

    @Value(("${tenant.enable}"))
    private Boolean enable;

    /**
     * &#x6CE8;&#x89E3;&#x65B9;&#x5F0F;&#x9ED8;&#x8BA4;&#x5173;&#x95ED;,&#x8FD9;&#x91CC;&#x53EA;&#x662F;&#x8BF4;&#x660E;&#x4E00;&#x79CD;&#x5B9E;&#x73B0;&#x65B9;&#x5F0F;&#xFF0C;&#x5B9E;&#x9645;&#x4F7F;&#x7528;&#x65F6;&#xFF0C;&#x4F7F;&#x7528;&#x914D;&#x7F6E;&#x7684;&#x65B9;&#x5F0F;&#x5373;&#x53EF;
     */
    @Value(("${data-permission.annotation-enable}"))
    private Boolean annotationEnable = false;

    private final RedisTemplate redisTemplate;

    public void processDataPermission(PlainSelect plainSelect, String mappedStatementId) {
        try {
            GitEggUser loginUser = GitEggAuthUtils.getCurrentUser();
            // 1 &#x5F53;&#x6709;&#x6570;&#x636E;&#x6743;&#x9650;&#x914D;&#x7F6E;&#x65F6;&#x624D;&#x53BB;&#x5224;&#x65AD;&#x7528;&#x6237;&#x662F;&#x5426;&#x6709;&#x6570;&#x636E;&#x6743;&#x9650;&#x63A7;&#x5236;
            if (ObjectUtils.isNotEmpty(loginUser) && CollectionUtils.isNotEmpty(loginUser.getDataPermissionTypeList())) {
                // 1 &#x6839;&#x636E;&#x7CFB;&#x7EDF;&#x914D;&#x7F6E;&#x7684;&#x6570;&#x636E;&#x6743;&#x9650;&#x62FC;&#x88C5;sql
                StringBuffer statementSb = new StringBuffer();
                if (enable)
                {
                    statementSb.append(DataPermissionConstant.TENANT_DATA_PERMISSION_KEY).append(loginUser.getTenantId());
                }
                else
                {
                    statementSb.append(DataPermissionConstant.DATA_PERMISSION_KEY);
                }
                String dataPermissionKey = statementSb.toString();
                StringBuffer statementSbt = new StringBuffer(DataPermissionConstant.DATA_PERMISSION_KEY_MAPPER);
                statementSbt.append(mappedStatementId).append(DataPermissionConstant.DATA_PERMISSION_KEY_TYPE);
                String mappedStatementIdKey = statementSbt.toString();
                DataPermissionEntity dataPermissionEntity = null;
                for (String dataPermissionType: loginUser.getDataPermissionTypeList())
                {
                    String dataPermissionUserKey = mappedStatementIdKey + dataPermissionType;
                    dataPermissionEntity = (DataPermissionEntity) redisTemplate.boundHashOps(dataPermissionKey).get(dataPermissionUserKey);
                    if (ObjectUtils.isNotEmpty(dataPermissionEntity)) {
                        break;
                    }
                }
                // mappedStatementId&#x662F;&#x5426;&#x6709;&#x914D;&#x7F6E;&#x6570;&#x636E;&#x6743;&#x9650;
                if (ObjectUtils.isNotEmpty(dataPermissionEntity))
                {
                    dataPermissionFilter(loginUser, dataPermissionEntity, plainSelect);
                }
                //&#x9ED8;&#x8BA4;&#x4E0D;&#x5F00;&#x542F;&#x6CE8;&#x89E3;&#xFF0C;&#x56E0;&#x6BCF;&#x6B21;&#x67E5;&#x8BE2;&#x90FD;&#x904D;&#x5386;&#x6CE8;&#x89E3;&#xFF0C;&#x5F71;&#x54CD;&#x6027;&#x80FD;&#xFF0C;&#x76F4;&#x63A5;&#x9009;&#x62E9;&#x4F7F;&#x7528;&#x914D;&#x7F6E;&#x7684;&#x65B9;&#x5F0F;&#x5B9E;&#x73B0;&#x6570;&#x636E;&#x6743;&#x9650;&#x5373;&#x53EF;
                else if(annotationEnable)
                {
                    // 2 &#x6839;&#x636E;&#x6CE8;&#x89E3;&#x7684;&#x6570;&#x636E;&#x6743;&#x9650;&#x62FC;&#x88C5;sql
                    Class<!--?--> clazz = Class.forName(mappedStatementId.substring(GitEggConstant.Number.ZERO, mappedStatementId.lastIndexOf(StringPool.DOT)));
                    String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + GitEggConstant.Number.ONE);
                    Method[] methods = clazz.getDeclaredMethods();
                    for (Method method : methods) {
                        //&#x5F53;&#x6709;&#x591A;&#x4E2A;&#x65F6;&#xFF0C;&#x8FD9;&#x4E2A;&#x65B9;&#x6CD5;&#x53EF;&#x4EE5;&#x83B7;&#x53D6;&#x5230;
                        DataPermission[] annotations = method.getAnnotationsByType(DataPermission.class);
                        if (ObjectUtils.isNotEmpty(annotations) && method.getName().equals(methodName)) {
                            for (DataPermission dataPermission : annotations) {
                                String dataPermissionType = dataPermission.dataPermissionType();
                                for (String dataPermissionUser : loginUser.getDataPermissionTypeList()) {
                                    if (ObjectUtils.isNotEmpty(dataPermission) && StringUtils.isNotEmpty(dataPermissionType)
                                            && dataPermissionUser.equals(dataPermissionType)) {
                                        DataPermissionEntity dataPermissionEntityAnnotation = annotationToEntity(dataPermission);
                                        dataPermissionFilter(loginUser, dataPermissionEntityAnnotation, plainSelect);
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }
            }

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * &#x6784;&#x5EFA;&#x8FC7;&#x6EE4;&#x6761;&#x4EF6;
     *
     * @param user &#x5F53;&#x524D;&#x767B;&#x5F55;&#x7528;&#x6237;
     * @param plainSelect plainSelect
     * @return &#x6784;&#x5EFA;&#x540E;&#x67E5;&#x8BE2;&#x6761;&#x4EF6;
     */
    public static void dataPermissionFilter(GitEggUser user, DataPermissionEntity dataPermissionEntity, PlainSelect plainSelect) {
        Expression expression = plainSelect.getWhere();
        String dataPermissionType = dataPermissionEntity.getDataPermissionType();
        String dataTableName = dataPermissionEntity.getDataTableName();
        String dataTableAlias = dataPermissionEntity.getDataTableAlias();

        String innerTableName = StringUtils.isNotEmpty(dataPermissionEntity.getInnerTableName()) ? dataPermissionEntity.getInnerTableName(): DataPermissionConstant.DATA_PERMISSION_TABLE_NAME;
        String innerTableAlias = StringUtils.isNotEmpty(dataPermissionEntity.getInnerTableAlias()) ? dataPermissionEntity.getInnerTableAlias() : DataPermissionConstant.DATA_PERMISSION_TABLE_ALIAS_NAME;

        List<string> organizationIdList = user.getOrganizationIdList();

        // &#x5217;&#x7EA7;&#x6570;&#x636E;&#x6743;&#x9650;
        String dataColumnExclude = dataPermissionEntity.getDataColumnExclude();
        String dataColumnInclude = dataPermissionEntity.getDataColumnInclude();
        List<string> includeColumns = new ArrayList<>();
        List<string> excludeColumns = new ArrayList<>();
        // &#x53EA;&#x5305;&#x542B;&#x8FD9;&#x51E0;&#x4E2A;&#x5B57;&#x6BB5;&#xFF0C;&#x4E5F;&#x5C31;&#x662F;&#x4E0D;&#x662F;&#x8FD9;&#x51E0;&#x4E2A;&#x5B57;&#x6BB5;&#x7684;&#xFF0C;&#x76F4;&#x63A5;&#x5220;&#x9664;
        if (StringUtils.isNotEmpty(dataColumnInclude))
        {
            includeColumns = Arrays.asList(dataColumnInclude.split(StringPool.COMMA));
        }

        // &#x9700;&#x8981;&#x6392;&#x9664;&#x8FD9;&#x51E0;&#x4E2A;&#x5B57;&#x6BB5;
        if (StringUtils.isNotEmpty(dataColumnExclude))
        {
            excludeColumns = Arrays.asList(dataColumnExclude.split(StringPool.COMMA));
        }
        List<selectitem> selectItems = plainSelect.getSelectItems();
        List<selectitem> removeItems = new ArrayList<>();
        if (CollectionUtils.isNotEmpty(selectItems)
                && (CollectionUtils.isNotEmpty(includeColumns) || CollectionUtils.isNotEmpty(excludeColumns))) {
            for (SelectItem selectItem : selectItems) {
                // &#x6682;&#x4E0D;&#x5904;&#x7406;&#x5176;&#x4ED6;&#x7C7B;&#x578B;&#x7684;selectItem
                if (selectItem instanceof SelectExpressionItem) {
                    SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
                    Alias alias = selectExpressionItem.getAlias();
                    if ((CollectionUtils.isNotEmpty(includeColumns) && !includeColumns.contains(alias.getName()))
                            || (!CollectionUtils.isEmpty(excludeColumns) && excludeColumns.contains(alias.getName())))
                    {
                        removeItems.add(selectItem);
                    }
                } else if (selectItem instanceof AllTableColumns) {
                    removeItems.add(selectItem);
                }
            }
            if (CollectionUtils.isNotEmpty(removeItems))
            {
                selectItems.removeAll(removeItems);
                plainSelect.setSelectItems(selectItems);
            }
        }

        // &#x884C;&#x7EA7;&#x6570;&#x636E;&#x6743;&#x9650;
        // &#x67E5;&#x8BE2;&#x7528;&#x6237;&#x673A;&#x6784;&#x548C;&#x5B50;&#x673A;&#x6784;&#x7684;&#x6570;&#x636E;&#xFF0C;&#x8FD9;&#x91CC;&#x662F;&#x4F7F;&#x7528;where&#x6761;&#x4EF6;&#x6DFB;&#x52A0;&#x5B50;&#x67E5;&#x8BE2;&#x7684;&#x65B9;&#x5F0F;&#x6765;&#x5B9E;&#x73B0;&#x7684;&#xFF0C;&#x8FD9;&#x6837;&#x7684;&#x5B9E;&#x73B0;&#x65B9;&#x5F0F;&#x597D;&#x5904;&#x662F;&#x4E0D;&#x9700;&#x8981;&#x5224;&#x65AD;Update&#xFF0C;Insert&#x8FD8;&#x662F;Select&#xFF0C;&#x90FD;&#x662F;&#x901A;&#x7528;&#x7684;&#xFF0C;&#x7F3A;&#x70B9;&#x662F;&#x6027;&#x80FD;&#x95EE;&#x9898;&#x3002;
        if (DataPermissionTypeEnum.DATA_PERMISSION_ORG_AND_CHILD.getLevel().equals(dataPermissionType)) {
            // &#x5982;&#x679C;&#x662F;table&#x7684;&#x8BDD;&#xFF0C;&#x90A3;&#x4E48;&#x76F4;&#x63A5;&#x52A0;inner&#xFF0C;&#x5982;&#x679C;&#x4E0D;&#x662F;&#xFF0C;&#x90A3;&#x4E48;&#x76F4;&#x63A5;&#x5728;where&#x6761;&#x4EF6;&#x91CC;&#x52A0;&#x5B50;&#x67E5;&#x8BE2;
            if (plainSelect.getFromItem() instanceof Table)
            {
                Table fromTable = (Table)plainSelect.getFromItem();
                //&#x6570;&#x636E;&#x4E3B;&#x8868;
                Table dataTable = null;
                //inner&#x6570;&#x636E;&#x6743;&#x9650;&#x8868;
                Table innerTable = null;
                if (fromTable.getName().equalsIgnoreCase(dataTableName))
                {
                    dataTable = (Table)plainSelect.getFromItem();
                }

                // &#x5982;&#x679C;&#x662F;&#x67E5;&#x8BE2;&#xFF0C;&#x8FD9;&#x91CC;&#x4F7F;&#x7528;inner join&#x5173;&#x8054;&#x8FC7;&#x6EE4;&#xFF0C;&#x4E0D;&#x4F7F;&#x7528;&#x5B50;&#x67E5;&#x8BE2;&#xFF0C;&#x56E0;&#x4E3A;join&#x4E0D;&#x9700;&#x8981;&#x5EFA;&#x7ACB;&#x4E34;&#x65F6;&#x8868;&#xFF0C;&#x56E0;&#x6B64;&#x901F;&#x5EA6;&#x6BD4;&#x5B50;&#x67E5;&#x8BE2;&#x5FEB;&#x3002;
                List<join> joins = plainSelect.getJoins();
                boolean hasPermissionTable = false;
                if (CollectionUtils.isNotEmpty(joins)) {
                    Iterator joinsIterator = joins.iterator();
                    while(joinsIterator.hasNext()) {
                        Join join = (Join)joinsIterator.next();
                        // &#x5224;&#x65AD;join&#x91CC;&#x9762;&#x662F;&#x5426;&#x5B58;&#x5728;t_sys_organization&#x8868;&#xFF0C;&#x5982;&#x679C;&#x5B58;&#x5728;&#xFF0C;&#x90A3;&#x4E48;&#x76F4;&#x63A5;&#x4F7F;&#x7528;&#xFF0C;&#x5982;&#x679C;&#x4E0D;&#x5B58;&#x5728;&#x5219;&#x65B0;&#x589E;
                        FromItem rightItem = join.getRightItem();
                        if (rightItem instanceof Table) {
                            Table table = (Table)rightItem;
                            // &#x5224;&#x65AD;&#x9700;&#x8981;inner&#x7684;&#x4E3B;&#x8868;&#x662F;&#x5426;&#x5B58;&#x5728;
                            if (null == dataTable && table.getName().equalsIgnoreCase(dataTableName))
                            {
                                dataTable = table;
                            }

                            // &#x5224;&#x65AD;&#x9700;&#x8981;inner&#x7684;&#x8868;&#x662F;&#x5426;&#x5B58;&#x5728;
                            if (table.getName().equalsIgnoreCase(innerTableName))
                            {
                                hasPermissionTable = true;
                                innerTable = table;
                            }
                        }
                    }
                }

                //&#x5982;&#x679C;&#x6CA1;&#x6709;&#x627E;&#x5230;&#x6570;&#x636E;&#x4E3B;&#x8868;&#xFF0C;&#x90A3;&#x4E48;&#x76F4;&#x63A5;&#x629B;&#x51FA;&#x5F02;&#x5E38;
                if (null == dataTable)
                {
                    throw new BusinessException("&#x5728;SQL&#x8BED;&#x53E5;&#x4E2D;&#x6CA1;&#x6709;&#x627E;&#x5230;&#x6570;&#x636E;&#x6743;&#x9650;&#x914D;&#x7F6E;&#x7684;&#x4E3B;&#x8868;&#xFF0C;&#x6570;&#x636E;&#x6743;&#x9650;&#x8FC7;&#x6EE4;&#x5931;&#x8D25;&#x3002;");
                }

                //&#x5982;&#x679C;&#x4E0D;&#x5B58;&#x5728;&#x8FD9;&#x4E2A;table&#xFF0C;&#x90A3;&#x4E48;&#x65B0;&#x589E;&#x4E00;&#x4E2A;innerjoin
                if (!hasPermissionTable)
                {
                    innerTable = new Table(innerTableName).withAlias(new Alias(innerTableAlias, false));
                    Join join = new Join();
                    join.withRightItem(innerTable);
                    EqualsTo equalsTo = new EqualsTo();
                    equalsTo.setLeftExpression(new Column(dataTable, DataPermissionConstant.DATA_PERMISSION_ORGANIZATION_ID));
                    equalsTo.setRightExpression(new Column(innerTable, DataPermissionConstant.DATA_PERMISSION_ID));
                    join.withOnExpression(equalsTo);
                    plainSelect.addJoins(join);
                }

                EqualsTo equalsToWhere = new EqualsTo();
                equalsToWhere.setLeftExpression(new Column(innerTable, DataPermissionConstant.DATA_PERMISSION_ID));
                equalsToWhere.setRightExpression(new LongValue(user.getOrganizationId()));
                Function function = new Function();
                function.setName(DataPermissionConstant.DATA_PERMISSION_FIND_IN_SET);
                function.setParameters(new ExpressionList(new LongValue(user.getOrganizationId()) , new Column(innerTable, DataPermissionConstant.DATA_PERMISSION_ANCESTORS)));
                OrExpression orExpression = new OrExpression(equalsToWhere, function);
                //&#x5224;&#x65AD;&#x662F;&#x5426;&#x6709;&#x6570;&#x636E;&#x6743;&#x9650;&#xFF0C;&#x5982;&#x679C;&#x6709;&#x6570;&#x636E;&#x6743;&#x9650;&#x914D;&#x7F6E;&#xFF0C;&#x90A3;&#x4E48;&#x6DFB;&#x52A0;&#x6570;&#x636E;&#x6743;&#x9650;&#x7684;&#x673A;&#x6784;&#x5217;&#x8868;
                if(CollectionUtils.isNotEmpty(organizationIdList))
                {
                    for (String organizationId : organizationIdList)
                    {
                        EqualsTo equalsToPermission = new EqualsTo();
                        equalsToPermission.setLeftExpression(new Column(innerTable, DataPermissionConstant.DATA_PERMISSION_ID));
                        equalsToPermission.setRightExpression(new LongValue(organizationId));
                        orExpression = new OrExpression(orExpression, equalsToPermission);
                        Function functionPermission = new Function();
                        functionPermission.setName(DataPermissionConstant.DATA_PERMISSION_FIND_IN_SET);
                        functionPermission.setParameters(new ExpressionList(new LongValue(organizationId) , new Column(innerTable,DataPermissionConstant.DATA_PERMISSION_ANCESTORS)));
                        orExpression = new OrExpression(orExpression, functionPermission);
                    }
                }
                expression = ObjectUtils.isNotEmpty(expression) ? new AndExpression(expression, new Parenthesis(orExpression)) : orExpression;
                plainSelect.setWhere(expression);
            }
            else
            {
                InExpression inExpression = new InExpression();
                inExpression.setLeftExpression(buildColumn(dataTableAlias, DataPermissionConstant.DATA_PERMISSION_ORGANIZATION_ID));
                SubSelect subSelect = new SubSelect();
                PlainSelect select = new PlainSelect();
                select.setSelectItems(Collections.singletonList(new SelectExpressionItem(new Column(DataPermissionConstant.DATA_PERMISSION_ID))));
                select.setFromItem(new Table(DataPermissionConstant.DATA_PERMISSION_TABLE_NAME));
                EqualsTo equalsTo = new EqualsTo();
                equalsTo.setLeftExpression(new Column(DataPermissionConstant.DATA_PERMISSION_ID));
                equalsTo.setRightExpression(new LongValue(user.getOrganizationId()));
                Function function = new Function();
                function.setName(DataPermissionConstant.DATA_PERMISSION_FIND_IN_SET);
                function.setParameters(new ExpressionList(new LongValue(user.getOrganizationId()) , new Column(DataPermissionConstant.DATA_PERMISSION_ANCESTORS)));
                OrExpression orExpression = new OrExpression(equalsTo, function);

                //&#x5224;&#x65AD;&#x662F;&#x5426;&#x6709;&#x6570;&#x636E;&#x6743;&#x9650;&#xFF0C;&#x5982;&#x679C;&#x6709;&#x6570;&#x636E;&#x6743;&#x9650;&#x914D;&#x7F6E;&#xFF0C;&#x90A3;&#x4E48;&#x6DFB;&#x52A0;&#x6570;&#x636E;&#x6743;&#x9650;&#x7684;&#x673A;&#x6784;&#x5217;&#x8868;
                if(CollectionUtils.isNotEmpty(organizationIdList))
                {
                    for (String organizationId : organizationIdList)
                    {
                        EqualsTo equalsToPermission = new EqualsTo();
                        equalsToPermission.setLeftExpression(new Column(DataPermissionConstant.DATA_PERMISSION_ID));
                        equalsToPermission.setRightExpression(new LongValue(organizationId));
                        orExpression = new OrExpression(orExpression, equalsToPermission);
                        Function functionPermission = new Function();
                        functionPermission.setName(DataPermissionConstant.DATA_PERMISSION_FIND_IN_SET);
                        functionPermission.setParameters(new ExpressionList(new LongValue(organizationId) , new Column(DataPermissionConstant.DATA_PERMISSION_ANCESTORS)));
                        orExpression = new OrExpression(orExpression, functionPermission);
                    }
                }
                select.setWhere(orExpression);
                subSelect.setSelectBody(select);
                inExpression.setRightExpression(subSelect);
                expression = ObjectUtils.isNotEmpty(expression) ? new AndExpression(expression, new Parenthesis(inExpression)) : inExpression;
                plainSelect.setWhere(expression);
            }
        }
        // &#x53EA;&#x67E5;&#x8BE2;&#x7528;&#x6237;&#x62E5;&#x6709;&#x673A;&#x6784;&#x7684;&#x6570;&#x636E;&#xFF0C;&#x4E0D;&#x5305;&#x542B;&#x5B50;&#x673A;&#x6784;
        else if (DataPermissionTypeEnum.DATA_PERMISSION_ORG.getLevel().equals(dataPermissionType)) {
            InExpression inExpression = new InExpression();
            inExpression.setLeftExpression(buildColumn(dataTableAlias, DataPermissionConstant.DATA_PERMISSION_ORGANIZATION_ID));
            ExpressionList expressionList = new ExpressionList();
            List<expression> expressions = new ArrayList<>();
            expressions.add(new LongValue(user.getOrganizationId()));
            if(CollectionUtils.isNotEmpty(organizationIdList))
            {
                for (String organizationId : organizationIdList)
                {
                    expressions.add(new LongValue(organizationId));
                }
            }
            expressionList.setExpressions(expressions);
            inExpression.setRightItemsList(expressionList);
            expression = ObjectUtils.isNotEmpty(expression) ? new AndExpression(expression, new Parenthesis(inExpression)) : inExpression;
            plainSelect.setWhere(expression);

        }
        // &#x53EA;&#x80FD;&#x67E5;&#x8BE2;&#x4E2A;&#x4EBA;&#x6570;&#x636E;
        else if (DataPermissionTypeEnum.DATA_PERMISSION_SELF.getLevel().equals(dataPermissionType)) {
            EqualsTo equalsTo = new EqualsTo();
            equalsTo.setLeftExpression(buildColumn(dataTableAlias, DataPermissionConstant.DATA_PERMISSION_SELF));
            equalsTo.setRightExpression(new StringValue(String.valueOf(user.getId())));
            expression = ObjectUtils.isNotEmpty(expression) ? new AndExpression(expression, new Parenthesis(equalsTo)) : equalsTo;
            plainSelect.setWhere(expression);
        }
        //&#x5F53;&#x7C7B;&#x578B;&#x4E3A;&#x67E5;&#x770B;&#x6240;&#x6709;&#x6570;&#x636E;&#x65F6;&#xFF0C;&#x4E0D;&#x5904;&#x7406;
//        if (DataPermissionTypeEnum.DATA_PERMISSION_ALL.getType().equals(dataPermissionType)) {
//
//        }
        // &#x81EA;&#x5B9A;&#x4E49;&#x8FC7;&#x6EE4;&#x8BED;&#x53E5;
        else if (DataPermissionTypeEnum.DATA_PERMISSION_CUSTOM.getLevel().equals(dataPermissionType)) {
            String customExpression = dataPermissionEntity.getCustomExpression();
            if (StringUtils.isEmpty(customExpression))
            {
                throw new BusinessException("&#x6CA1;&#x6709;&#x914D;&#x7F6E;&#x81EA;&#x5B9A;&#x4E49;&#x8868;&#x8FBE;&#x5F0F;");
            }
            try {
                Expression expressionCustom = CCJSqlParserUtil.parseCondExpression(customExpression);
                expression = ObjectUtils.isNotEmpty(expression) ? new AndExpression(expression, new Parenthesis(expressionCustom)) : expressionCustom;
                plainSelect.setWhere(expression);
            } catch (JSQLParserException e) {
                throw new BusinessException("&#x81EA;&#x5B9A;&#x4E49;&#x8868;&#x8FBE;&#x5F0F;&#x914D;&#x7F6E;&#x9519;&#x8BEF;");
            }
        }
    }

    /**
     * &#x6784;&#x5EFA;Column
     *
     * @param dataTableAlias &#x8868;&#x522B;&#x540D;
     * @param columnName &#x5B57;&#x6BB5;&#x540D;&#x79F0;
     * @return &#x5E26;&#x8868;&#x522B;&#x540D;&#x5B57;&#x6BB5;
     */
    public static Column buildColumn(String dataTableAlias, String columnName) {
        if (StringUtils.isNotEmpty(dataTableAlias)) {
            columnName = dataTableAlias + StringPool.DOT + columnName;
        }
        return new Column(columnName);
    }

    /**
     * &#x6CE8;&#x89E3;&#x8F6C;&#x4E3A;&#x5B9E;&#x4F53;&#x7C7B;
     * @param annotation &#x6CE8;&#x89E3;
     * @return &#x5B9E;&#x4F53;&#x7C7B;
     */
    public static DataPermissionEntity annotationToEntity(DataPermission annotation) {
        DataPermissionEntity dataPermissionEntity = new DataPermissionEntity();
        dataPermissionEntity.setDataPermissionType(annotation.dataPermissionType());
        dataPermissionEntity.setDataColumnExclude(annotation.dataColumnExclude());
        dataPermissionEntity.setDataColumnInclude(annotation.dataColumnInclude());
        dataPermissionEntity.setDataTableName(annotation.dataTableName());
        dataPermissionEntity.setDataTableAlias(annotation.dataTableAlias());
        dataPermissionEntity.setInnerTableName(annotation.innerTableName());
        dataPermissionEntity.setInnerTableAlias(annotation.innerTableAlias());
        dataPermissionEntity.setCustomExpression(annotation.customExpression());
        return dataPermissionEntity;
    }

    @Override
    public Expression getSqlSegment(Expression where, String mappedStatementId) {
        return null;
    }
</expression></join></selectitem></selectitem></string></string></string>
  • 系统启动时初始化数据权限配置到Redis
    @Override
    public void initDataRolePermissions() {
        List<datapermissionroledto> dataPermissionRoleList = dataPermissionRoleMapper.queryDataPermissionRoleListAll();
        // &#x5224;&#x65AD;&#x662F;&#x5426;&#x5F00;&#x542F;&#x4E86;&#x79DF;&#x6237;&#x6A21;&#x5F0F;&#xFF0C;&#x5982;&#x679C;&#x5F00;&#x542F;&#x4E86;&#xFF0C;&#x90A3;&#x4E48;&#x89D2;&#x8272;&#x6743;&#x9650;&#x9700;&#x8981;&#x6309;&#x79DF;&#x6237;&#x8FDB;&#x884C;&#x5206;&#x7C7B;&#x5B58;&#x50A8;
        if (enable) {
            Map<long, list<datapermissionroledto>> dataPermissionRoleListMap =
                    dataPermissionRoleList.stream().collect(Collectors.groupingBy(DataPermissionRoleDTO::getTenantId));
            dataPermissionRoleListMap.forEach((key, value) -> {
                // auth:tenant:data:permission:0
                String redisKey = DataPermissionConstant.TENANT_DATA_PERMISSION_KEY + key;
                redisTemplate.delete(redisKey);
                addDataRolePermissions(redisKey, value);
            });
        } else {
            redisTemplate.delete(DataPermissionConstant.DATA_PERMISSION_KEY);
            // auth:data:permission
            addDataRolePermissions(DataPermissionConstant.DATA_PERMISSION_KEY, dataPermissionRoleList);
        }
    }

    private void addDataRolePermissions(String key, List<datapermissionroledto> dataPermissionRoleList) {
        Map<string, datapermissionentity> dataPermissionMap = new TreeMap<>();
        Optional.ofNullable(dataPermissionRoleList).orElse(new ArrayList<>()).forEach(dataPermissionRole -> {
            String dataRolePermissionCache = new StringBuffer(DataPermissionConstant.DATA_PERMISSION_KEY_MAPPER)
                    .append(dataPermissionRole.getDataMapperFunction()).append(DataPermissionConstant.DATA_PERMISSION_KEY_TYPE)
                    .append(dataPermissionRole.getDataPermissionType()).toString();
            DataPermissionEntity dataPermissionEntity = BeanCopierUtils.copyByClass(dataPermissionRole, DataPermissionEntity.class);
            dataPermissionMap.put(dataRolePermissionCache, dataPermissionEntity);
        });
        redisTemplate.boundHashOps(key).putAll(dataPermissionMap);
    }
</string,></datapermissionroledto></long,></datapermissionroledto>
数据权限配置指南:

SpringCloud微服务实战——搭建企业级开发框架(二十八):扩展MybatisPlus插件DataPermissionInterceptor实现数据权限控制
  • 数据权限名称:自定义一个名称,方便查找和区分
  • Mapper全路径: Mapper路径配置到具体方法名称,例:com.gitegg.service.system.mapper.UserMapper.selectUserList
  • 数据权限类型:
    只能查看本人(实现原理是在查询条件添加数据表的creator条件)
    只能查看本部门 (实现原理是在查询条件添加数据表的部门条件)
    只能查看本部门及子部门 (实现原理是在查询条件添加数据表的部门条件)
    可以查看所有数据(不处理)
    自定义(添加where子条件)
注解配置数据权限配置指南:
    /**
     * &#x67E5;&#x8BE2;&#x7528;&#x6237;&#x5217;&#x8868;
     * @param page
     * @param user
     * @return
     */
    @DataPermission(dataTableName = "t_sys_organization_user", dataTableAlias = "organizationUser", dataPermissionType = "3", innerTableName = "t_sys_organization", innerTableAlias = "orgDataPermission")
    @DataPermission(dataTableName = "t_sys_organization_user", dataTableAlias = "organizationUser", dataPermissionType = "2", innerTableName = "t_sys_organization", innerTableAlias = "orgDataPermission")
    @DataPermission(dataTableName = "t_sys_organization_user", dataTableAlias = "organizationUser", dataPermissionType = "1", innerTableName = "t_sys_organization", innerTableAlias = "orgDataPermission")
    Page<userinfo> selectUserList(Page<userinfo> page, @Param("user") QueryUserDTO user);
</userinfo></userinfo>
行级数据权限配置:

数据主表:主数据表,用于数据操作时的主表,例如SQL语句时的主表
数据主表别名:主数据表的别名,用于和数据权限表进行inner join操作
数据权限表:用于inner join的数据权限表,主要用于使用ancestors字段查询所有子组织机构
数据权限表别名:用于和主数据表进行inner join

列级数据权限配置:

排除的字段:配置没有权限查看的字段,需要排除这些字段
保留的字段:配置有权限查看的字段,只保留这些字段

备注:
  • 此数据权限设计较灵活,也较复杂,有些简单应用场景的系统可能根本用不到,只需配置行级数据权限即可。
  • Mybatis-Plus的插件DataPermissionInterceptor使用说明https://gitee.com/baomidou/mybatis-plus/issues/I37I90
  • update,insert逻辑说明:inner时只支持正常查询,及inner查询,不支持子查询,update,insert,子查询等直接使用添加子查询的方式实现数据权限
  • 还有在这里说明一下,在我们实际业务开发过程中,只能查看本人数据的数据权限,一般不会通过系统来配置,而是在业务代码编写过程中就 会实现,比如查询个人订单接口,那么个人用户id肯定是接口的入参,在接口被请求的时候,只需要通过我们自定义的方法获取到当前登录用户,然后作为参数传入即可。这种对于个人数据的数据权限,通过业务代码来实现会更加方便和安全,且没有太多的工作量,方便理解也容易扩展。
源码地址:

Gitee: https://gitee.com/wmz1930/GitEgg

GitHub: https://github.com/wmz1930/GitEgg

Original: https://www.cnblogs.com/FullStackProgrammer/p/15637614.html
Author: 全栈程序猿
Title: SpringCloud微服务实战——搭建企业级开发框架(二十八):扩展MybatisPlus插件DataPermissionInterceptor实现数据权限控制

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/590159/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 字典生成

    crunch的使用 依然是熟悉的console-crunch-xxx min:设定最小字符串长度 max:设定最大字符串长度 -o:将生成的字典保存到指定文件 -t:指定密码输出的…

    Java 2023年6月7日
    071
  • Golang中的数组、切片以及map

    一、固定长度数组 1.定义长度固定的数组,如:var arr [10] int;获取数组的长度,使用len函数,即len(arr)。 2.定义长度固定的数组,且初始化,如:arr …

    Java 2023年6月13日
    075
  • JAVA开发工具的具体安装与使用

    变量名:MYSQL_HOME 变量值:mysql安装路径 在点击path加入%MYSQL_HOME%\bin 最后命令窗口查看版本mysql –version 命令窗口…

    Java 2023年6月9日
    073
  • (转发)Debian GNU/Linux 11.1 安装

    Debian GNU/Linux 是 Linux 发行版之一,也是众多 Linux 发行版的上游系统,下游Linux 发行版,比如 Ubuntu,可视为 Debian 的改动款。当…

    Java 2023年6月9日
    0105
  • org.springframework.web.method.HandlerMethod 与 org.springframework.messaging.handler.HandlerMethod 转换失败

    Springmvc hander.getclassclass org.springframework.web.method.HandlerMethod HandlerMethod….

    Java 2023年5月30日
    050
  • Java面试题整理

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    Java 2023年6月7日
    075
  • springcloud技术栈系列_5单元和接口自动化测试

    作为IT行业的从业者,质量对你的影响贯穿整个职业生涯。 我想给各个不同岗位的研发相关同学提出一些问题。 假如你是Java工程师: 如何主动的提高代码质量?设计上,单测工具使用上有哪…

    Java 2023年6月8日
    064
  • Spring入门笔记简要

    &#x603B;&#x89C8;&#xFF1A;https://www.processon.com/view/link/605b14ab5653bb2225…

    Java 2023年6月13日
    096
  • java去除字符串中的空格t、回车n、换行符r、制表符t

    \n 回车(\u000a)\t 水平制表符(\u0009)\s 空格(\u0008)\r 换行(\u000d)在日常开发中,空格、回车、换行符等一些特殊字符经常会困扰着我们对于数据…

    Java 2023年5月29日
    047
  • Java面向对象

    Java的核心思想就是OOP 面向过程 & 面向对象 面向过程思想 步骤清晰简单,第一步做什么,第二步做什么… 面对过程适合处理一些较为简单的问题* 面向对象思…

    Java 2023年6月5日
    064
  • MySQL中Join和inner join的区别,以及left join、right join之间的区别

    点击阅读 本文来自博客园,作者:一个程序员的成长,转载请注明原文链接:https://www.cnblogs.com/bingfengdada/p/15539727.html Or…

    Java 2023年6月5日
    052
  • Effective Java 第三版—— 84. 不要依赖线程调度器

    Tips书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code注意,书中的有些代码里方法是基于Java 9…

    Java 2023年5月29日
    085
  • Fizz网关入门教程-快速聚合多接口

    背景介绍 先看一张图,页面一共调用了20多个后端接口,页面出现了明显的卡顿,而且每6个请求一组,只有前一组返回后才执行下一组。看到请求是pending状态,很容易以为是后端接口的问…

    Java 2023年6月9日
    085
  • 数据结构学习笔记

    数据结构学习笔记 数据结构=个体的存储+个体的关系存储 算法=对存储数据的操作 数据结构是专门研究数据存储的问题 狭义的算法是与数据的存储方式密切相关;广义的算法是与数据的存储方式…

    Java 2023年6月7日
    072
  • 安装ubuntu

    ubuntu是一个以桌面应用为主的开源CNU/Linux操作系统,专业的Python开发者一般会选择Ubuntu这款Linux系统作为生产平台. 安装地址 https://cn.u…

    Java 2023年6月5日
    080
  • python 图形验证码的实现

    python 图形验证码的实现 导入pil库和ramdom库 from PIL import Image, ImageDraw, ImageFont, ImageFilter im…

    Java 2023年6月7日
    054
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球