SpringCloud微服務(wù)實(shí)戰(zhàn)——搭建企業(yè)級(jí)開發(fā)框架(二十八):擴(kuò)展MybatisPlus插件DataPermissionInterceptor實(shí)現(xiàn)數(shù)據(jù)權(quán)限控制

??一套完整的系統(tǒng)權(quán)限需要支持功能權(quán)限和數(shù)據(jù)權(quán)限,前面介紹了系統(tǒng)通過RBAC的權(quán)限模型來實(shí)現(xiàn)功能的權(quán)限控制,這里我們來介紹,通過擴(kuò)展Mybatis-Plus的插件DataPermissionInterceptor實(shí)現(xiàn)數(shù)據(jù)權(quán)限控制。

??簡(jiǎn)單介紹一下,所謂功能權(quán)限,顧名思義是指用戶在系統(tǒng)中擁有對(duì)哪些功能操作的權(quán)限控制,而數(shù)據(jù)權(quán)限是指用戶在系統(tǒng)中能夠訪問哪些數(shù)據(jù)的權(quán)限控制,數(shù)據(jù)權(quán)限又分為行級(jí)數(shù)據(jù)權(quán)限和列級(jí)數(shù)據(jù)權(quán)限。

數(shù)據(jù)權(quán)限基本概念:

  • 行級(jí)數(shù)據(jù)權(quán)限:以表結(jié)構(gòu)為描述對(duì)象,一個(gè)用戶擁有對(duì)哪些數(shù)據(jù)的權(quán)限,表示為對(duì)數(shù)據(jù)庫某個(gè)表整行的數(shù)據(jù)擁有權(quán)限,例如按部門區(qū)分,某一行數(shù)據(jù)屬于某個(gè)部門,某個(gè)用戶只對(duì)此部門的數(shù)據(jù)擁有權(quán)限,那么該用戶擁有此行的數(shù)據(jù)權(quán)限。
  • 列級(jí)數(shù)據(jù)權(quán)限:以表結(jié)構(gòu)為描述對(duì)象,一個(gè)用戶可能只對(duì)某個(gè)表中的部分字段擁有權(quán)限,例如表中銀行卡、手機(jī)號(hào)等重要信息只有高級(jí)用戶能夠查詢,而一些基本信息,普通用戶就可以查詢,不同的用戶角色擁有的數(shù)據(jù)權(quán)限不一樣。

實(shí)現(xiàn)方式:

  • 行級(jí)數(shù)據(jù)權(quán)限:
    ??對(duì)行級(jí)數(shù)據(jù)權(quán)限進(jìn)行細(xì)分,以角色為標(biāo)識(shí)的數(shù)據(jù)權(quán)限,分為:
    ??1、只能查看本人數(shù)據(jù);
    ??2、只能查看本部門數(shù)據(jù);
    ??3、只能查看本部門及子部門數(shù)據(jù);
    ??4、可以查看所有部門數(shù)據(jù);
    ??以用戶為標(biāo)識(shí)的數(shù)據(jù)權(quán)限,分為:
    ??5、同一功能角色權(quán)限擁有不同部門的數(shù)據(jù)權(quán)限;
    ??6、不同角色權(quán)限擁有不同部門的數(shù)據(jù)權(quán)限。
    ??第1/2/3/4類的實(shí)現(xiàn)方式需要在角色列表對(duì)角色進(jìn)行數(shù)據(jù)權(quán)限配置,針對(duì)某一接口該角色擁有哪種數(shù)據(jù)權(quán)限。
    ??第5類的實(shí)現(xiàn)方式,需要在用戶列表進(jìn)行配置,給用戶分配多個(gè)不同部門。
    ??第6類的實(shí)現(xiàn)方式比較復(fù)雜,目前有市面上的大多數(shù)解決方案是:
    ????1、在登錄時(shí),判斷用戶是否擁有多個(gè)部門,如果存在,那么首先讓用戶選擇其所在的部門,登錄后只對(duì)選擇的部門權(quán)限進(jìn)行操作;
    ????2、針對(duì)不同部門創(chuàng)建不同的用戶及角色,登錄時(shí),選擇對(duì)應(yīng)的賬號(hào)進(jìn)行登錄。
    ??個(gè)人因秉承復(fù)雜的系統(tǒng)簡(jiǎn)單化,盡量用低耦合的方式實(shí)現(xiàn)復(fù)雜功能的理念,更傾向于第二種方式,原因是:
??1、系統(tǒng)實(shí)現(xiàn)方面減少復(fù)雜度,越復(fù)雜的判斷,越容易出問題,不僅僅在開發(fā)過程中,還在于后續(xù)系統(tǒng)的擴(kuò)展和更新過程中。
??2、對(duì)于工作量方面的取舍,一個(gè)人擁有多個(gè)部門不同權(quán)限的方式屬于常用功能,但是并不普遍,也就是說在一家企業(yè)中,同一個(gè)用戶即是業(yè)務(wù)部門經(jīng)理,又是財(cái)務(wù)部門經(jīng)理的情況并不普遍,更多的是專人專職。這里要和第5類做好區(qū)分,比如你是業(yè)務(wù)部門經(jīng)理可能會(huì)管理多個(gè)部門,這種屬于權(quán)限一致,只是擁有多個(gè)部門權(quán)限,這屬于第5類。再比如一個(gè)總經(jīng)理,可能會(huì)看到所有的業(yè)務(wù)、財(cái)務(wù)數(shù)據(jù)這屬于第4類。
??所以這里不會(huì)采取用戶登錄后選擇部門的方式來判斷數(shù)據(jù)權(quán)限。
  • 列級(jí)數(shù)據(jù)權(quán)限:
    ??列級(jí)數(shù)據(jù)權(quán)限的實(shí)現(xiàn)主要是針對(duì)某個(gè)角色能夠看到哪些字段,不存在針對(duì)某個(gè)用戶給他特定字段的情況,這種情況單獨(dú)建立一個(gè)角色即可,盡量采用類RBAC的方式來實(shí)現(xiàn),不要使用戶直接和數(shù)據(jù)權(quán)限關(guān)聯(lián)。列級(jí)數(shù)據(jù)權(quán)限除了要考慮后臺(tái)取數(shù)據(jù)的問題,還要考慮到在界面上展示時(shí),如果是一個(gè)表格,那么沒有權(quán)限的列需要根據(jù)數(shù)據(jù)權(quán)限來判斷是否展示。這里在配置界面就要考慮,在角色配置時(shí),需要分為行級(jí)數(shù)據(jù)權(quán)限和列級(jí)數(shù)據(jù)權(quán)限進(jìn)行不同的配置:行級(jí)數(shù)據(jù)權(quán)限應(yīng)該配置需要數(shù)據(jù)權(quán)限控制的接口,數(shù)據(jù)權(quán)限的類型(上面提到的1234);列級(jí)數(shù)據(jù)權(quán)限除了需要配置上面提到的之外,還需要配置可以訪問的字段或者排除訪問的字段。


    數(shù)據(jù)權(quán)限
在資源管理配置資源關(guān)聯(lián)接口的數(shù)據(jù)權(quán)限規(guī)則(t_sys_data_permission_role),通過RBAC的方式用角色和用戶關(guān)聯(lián),在用戶管理配置用戶同角色的多個(gè)部門數(shù)據(jù)權(quán)限,用戶直接和部門關(guān)聯(lián)(t_sys_data_permission_user)。系統(tǒng)數(shù)據(jù)權(quán)限管理功能設(shè)計(jì)如下所示:
權(quán)限管理
數(shù)據(jù)權(quán)限表設(shè)計(jì):
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 '機(jī)構(gòu)id',
  `status` tinyint(2) NULL DEFAULT 1 COMMENT '狀態(tài) 0禁用,1 啟用,',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '創(chuàng)建時(shí)間',
  `creator` bigint(20) NULL DEFAULT NULL COMMENT '創(chuàng)建者',
  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新時(shí)間',
  `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 '功能權(quán)限id',
  `data_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '數(shù)據(jù)權(quán)限名稱',
  `data_mapper_function` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '數(shù)據(jù)權(quán)限對(duì)應(yīng)的mapper方法全路徑',
  `data_table_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '需要做數(shù)據(jù)權(quán)限主表',
  `data_table_alias` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '需要做數(shù)據(jù)權(quán)限表的別名',
  `data_column_exclude` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '數(shù)據(jù)權(quán)限需要排除的字段',
  `data_column_include` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '數(shù)據(jù)權(quán)限需要保留的字段',
  `inner_table_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '數(shù)據(jù)權(quán)限表,默認(rèn)t_sys_organization',
  `inner_table_alias` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '數(shù)據(jù)權(quán)限表的別名,默認(rèn)organization',
  `data_permission_type` varchar(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '1' COMMENT '數(shù)據(jù)權(quán)限類型:1只能查看本人 2只能查看本部門 3只能查看本部門及子部門 4可以查看所有數(shù)據(jù)',
  `custom_expression` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '自定義數(shù)據(jù)權(quán)限(增加 where條件)',
  `status` tinyint(2) NOT NULL DEFAULT 1 COMMENT '狀態(tài) 0禁用,1 啟用,',
  `comments` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '備注',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '創(chuàng)建時(shí)間',
  `creator` bigint(20) NULL DEFAULT NULL COMMENT '創(chuàng)建者',
  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新時(shí)間',
  `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 = '數(shù)據(jù)權(quán)限配置表' ROW_FORMAT = DYNAMIC;

數(shù)據(jù)權(quán)限緩存(Redis)設(shè)計(jì):
  • Redis Key:
    多租戶模式:auth:tenant:data:permission:0(租戶):mapper_Mapper全路徑type數(shù)據(jù)權(quán)限類型
    普通模式:auth:data:permission:mapper_Mapper全路徑type數(shù)據(jù)權(quán)限類型
  • Redis Value:存放角色分配的DataPermissionEntity配置
    ??數(shù)據(jù)權(quán)限插件在組裝SQL時(shí),首先通過前綴匹配查詢mapper的statementId是否在緩存中,如果存在,那么取出當(dāng)前用戶的數(shù)據(jù)權(quán)限類型,組裝好帶有數(shù)據(jù)權(quán)限類型的DataPermission緩存Key,從緩存中取出數(shù)據(jù)權(quán)限配置。
    在設(shè)計(jì)角色時(shí),除了需要給角色設(shè)置功能權(quán)限之外,還要設(shè)置數(shù)據(jù)權(quán)限類型,角色的數(shù)據(jù)權(quán)限類型只能單選(1只能查看本人 2只能查看本部門 3只能查看本部門及子部門 4可以查看所有數(shù)據(jù)5自定義)
代碼實(shí)現(xiàn):
  • 因DataPermissionInterceptor默認(rèn)不支持修改selectItems,導(dǎo)致無法做到列級(jí)別的數(shù)據(jù)權(quán)限,所以這里自定義擴(kuò)展DataPermissionInterceptor,使其支持列級(jí)權(quán)限擴(kuò)展
@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);
    }

}
  • 自定義實(shí)現(xiàn)DataPermissionHandler數(shù)據(jù)權(quán)限控制
@Component
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class GitEggDataPermissionHandler implements DataPermissionHandler {

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

    /**
     * 注解方式默認(rèn)關(guān)閉,這里只是說明一種實(shí)現(xiàn)方式,實(shí)際使用時(shí),使用配置的方式即可
     */
    @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 當(dāng)有數(shù)據(jù)權(quán)限配置時(shí)才去判斷用戶是否有數(shù)據(jù)權(quán)限控制
            if (ObjectUtils.isNotEmpty(loginUser) && CollectionUtils.isNotEmpty(loginUser.getDataPermissionTypeList())) {
                // 1 根據(jù)系統(tǒng)配置的數(shù)據(jù)權(quán)限拼裝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是否有配置數(shù)據(jù)權(quán)限
                if (ObjectUtils.isNotEmpty(dataPermissionEntity))
                {
                    dataPermissionFilter(loginUser, dataPermissionEntity, plainSelect);
                }
                //默認(rèn)不開啟注解,因每次查詢都遍歷注解,影響性能,直接選擇使用配置的方式實(shí)現(xiàn)數(shù)據(jù)權(quán)限即可
                else if(annotationEnable)
                {
                    // 2 根據(jù)注解的數(shù)據(jù)權(quán)限拼裝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) {
                        //當(dāng)有多個(gè)時(shí),這個(gè)方法可以獲取到
                        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();
        }
    }

    /**
     * 構(gòu)建過濾條件
     *
     * @param user 當(dāng)前登錄用戶
     * @param plainSelect plainSelect
     * @return 構(gòu)建后查詢條件
     */
    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();

        // 列級(jí)數(shù)據(jù)權(quán)限
        String dataColumnExclude = dataPermissionEntity.getDataColumnExclude();
        String dataColumnInclude = dataPermissionEntity.getDataColumnInclude();
        List<String> includeColumns = new ArrayList<>();
        List<String> excludeColumns = new ArrayList<>();
        // 只包含這幾個(gè)字段,也就是不是這幾個(gè)字段的,直接刪除
        if (StringUtils.isNotEmpty(dataColumnInclude))
        {
            includeColumns = Arrays.asList(dataColumnInclude.split(StringPool.COMMA));
        }

        // 需要排除這幾個(gè)字段
        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) {
                // 暫不處理其他類型的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);
            }
        }

        // 行級(jí)數(shù)據(jù)權(quán)限
        // 查詢用戶機(jī)構(gòu)和子機(jī)構(gòu)的數(shù)據(jù),這里是使用where條件添加子查詢的方式來實(shí)現(xiàn)的,這樣的實(shí)現(xiàn)方式好處是不需要判斷Update,Insert還是Select,都是通用的,缺點(diǎn)是性能問題。
        if (DataPermissionTypeEnum.DATA_PERMISSION_ORG_AND_CHILD.getLevel().equals(dataPermissionType)) {
            // 如果是table的話,那么直接加inner,如果不是,那么直接在where條件里加子查詢
            if (plainSelect.getFromItem() instanceof Table)
            {
                Table fromTable = (Table)plainSelect.getFromItem();
                //數(shù)據(jù)主表
                Table dataTable = null;
                //inner數(shù)據(jù)權(quán)限表
                Table innerTable = null;
                if (fromTable.getName().equalsIgnoreCase(dataTableName))
                {
                    dataTable = (Table)plainSelect.getFromItem();
                }

                // 如果是查詢,這里使用inner join關(guān)聯(lián)過濾,不使用子查詢,因?yàn)閖oin不需要建立臨時(shí)表,因此速度比子查詢快。
                List<Join> joins = plainSelect.getJoins();
                boolean hasPermissionTable = false;
                if (CollectionUtils.isNotEmpty(joins)) {
                    Iterator joinsIterator = joins.iterator();
                    while(joinsIterator.hasNext()) {
                        Join join = (Join)joinsIterator.next();
                        // 判斷join里面是否存在t_sys_organization表,如果存在,那么直接使用,如果不存在則新增
                        FromItem rightItem = join.getRightItem();
                        if (rightItem instanceof Table) {
                            Table table = (Table)rightItem;
                            // 判斷需要inner的主表是否存在
                            if (null == dataTable && table.getName().equalsIgnoreCase(dataTableName))
                            {
                                dataTable = table;
                            }

                            // 判斷需要inner的表是否存在
                            if (table.getName().equalsIgnoreCase(innerTableName))
                            {
                                hasPermissionTable = true;
                                innerTable = table;
                            }
                        }
                    }
                }

                //如果沒有找到數(shù)據(jù)主表,那么直接拋出異常
                if (null == dataTable)
                {
                    throw new BusinessException("在SQL語句中沒有找到數(shù)據(jù)權(quán)限配置的主表,數(shù)據(jù)權(quán)限過濾失敗。");
                }

                //如果不存在這個(gè)table,那么新增一個(gè)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);
                //判斷是否有數(shù)據(jù)權(quán)限,如果有數(shù)據(jù)權(quán)限配置,那么添加數(shù)據(jù)權(quán)限的機(jī)構(gòu)列表
                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);

                //判斷是否有數(shù)據(jù)權(quán)限,如果有數(shù)據(jù)權(quán)限配置,那么添加數(shù)據(jù)權(quán)限的機(jī)構(gòu)列表
                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);
            }
        }
        // 只查詢用戶擁有機(jī)構(gòu)的數(shù)據(jù),不包含子機(jī)構(gòu)
        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);

        }
        // 只能查詢個(gè)人數(shù)據(jù)
        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);
        }
        //當(dāng)類型為查看所有數(shù)據(jù)時(shí),不處理
//        if (DataPermissionTypeEnum.DATA_PERMISSION_ALL.getType().equals(dataPermissionType)) {
//
//        }
        // 自定義過濾語句
        else if (DataPermissionTypeEnum.DATA_PERMISSION_CUSTOM.getLevel().equals(dataPermissionType)) {
            String customExpression = dataPermissionEntity.getCustomExpression();
            if (StringUtils.isEmpty(customExpression))
            {
                throw new BusinessException("沒有配置自定義表達(dá)式");
            }
            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("自定義表達(dá)式配置錯(cuò)誤");
            }
        }
    }

    /**
     * 構(gòu)建Column
     *
     * @param dataTableAlias 表別名
     * @param columnName 字段名稱
     * @return 帶表別名字段
     */
    public static Column buildColumn(String dataTableAlias, String columnName) {
        if (StringUtils.isNotEmpty(dataTableAlias)) {
            columnName = dataTableAlias + StringPool.DOT + columnName;
        }
        return new Column(columnName);
    }


    /**
     * 注解轉(zhuǎn)為實(shí)體類
     * @param annotation 注解
     * @return 實(shí)體類
     */
    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;
    }
  • 系統(tǒng)啟動(dòng)時(shí)初始化數(shù)據(jù)權(quán)限配置到Redis
    @Override
    public void initDataRolePermissions() {
        List<DataPermissionRoleDTO> dataPermissionRoleList = dataPermissionRoleMapper.queryDataPermissionRoleListAll();
        // 判斷是否開啟了租戶模式,如果開啟了,那么角色權(quán)限需要按租戶進(jìn)行分類存儲(chǔ)
        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);
    }
數(shù)據(jù)權(quán)限配置指南:
image.png
  • 數(shù)據(jù)權(quán)限名稱:自定義一個(gè)名稱,方便查找和區(qū)分
  • Mapper全路徑: Mapper路徑配置到具體方法名稱,例:com.gitegg.service.system.mapper.UserMapper.selectUserList
  • 數(shù)據(jù)權(quán)限類型:
    只能查看本人(實(shí)現(xiàn)原理是在查詢條件添加數(shù)據(jù)表的creator條件)
    只能查看本部門 (實(shí)現(xiàn)原理是在查詢條件添加數(shù)據(jù)表的部門條件)
    只能查看本部門及子部門 (實(shí)現(xiàn)原理是在查詢條件添加數(shù)據(jù)表的部門條件)
    可以查看所有數(shù)據(jù)(不處理)
    自定義(添加where子條件)
注解配置數(shù)據(jù)權(quán)限配置指南:
    /**
     * 查詢用戶列表
     * @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);
行級(jí)數(shù)據(jù)權(quán)限配置:

數(shù)據(jù)主表:主數(shù)據(jù)表,用于數(shù)據(jù)操作時(shí)的主表,例如SQL語句時(shí)的主表
數(shù)據(jù)主表別名:主數(shù)據(jù)表的別名,用于和數(shù)據(jù)權(quán)限表進(jìn)行inner join操作
數(shù)據(jù)權(quán)限表:用于inner join的數(shù)據(jù)權(quán)限表,主要用于使用ancestors字段查詢所有子組織機(jī)構(gòu)
數(shù)據(jù)權(quán)限表別名:用于和主數(shù)據(jù)表進(jìn)行inner join

列級(jí)數(shù)據(jù)權(quán)限配置:

排除的字段:配置沒有權(quán)限查看的字段,需要排除這些字段
保留的字段:配置有權(quán)限查看的字段,只保留這些字段

備注:
  • 此數(shù)據(jù)權(quán)限設(shè)計(jì)較靈活,也較復(fù)雜,有些簡(jiǎn)單應(yīng)用場(chǎng)景的系統(tǒng)可能根本用不到,只需配置行級(jí)數(shù)據(jù)權(quán)限即可。
  • Mybatis-Plus的插件DataPermissionInterceptor使用說明 https://gitee.com/baomidou/mybatis-plus/issues/I37I90
  • update,insert邏輯說明:inner時(shí)只支持正常查詢,及inner查詢,不支持子查詢,update,insert,子查詢等直接使用添加子查詢的方式實(shí)現(xiàn)數(shù)據(jù)權(quán)限
  • 還有在這里說明一下,在我們實(shí)際業(yè)務(wù)開發(fā)過程中,只能查看本人數(shù)據(jù)的數(shù)據(jù)權(quán)限,一般不會(huì)通過系統(tǒng)來配置,而是在業(yè)務(wù)代碼編寫過程中就 會(huì)實(shí)現(xiàn),比如查詢個(gè)人訂單接口,那么個(gè)人用戶id肯定是接口的入?yún)?,在接口被?qǐng)求的時(shí)候,只需要通過我們自定義的方法獲取到當(dāng)前登錄用戶,然后作為參數(shù)傳入即可。這種對(duì)于個(gè)人數(shù)據(jù)的數(shù)據(jù)權(quán)限,通過業(yè)務(wù)代碼來實(shí)現(xiàn)會(huì)更加方便和安全,且沒有太多的工作量,方便理解也容易擴(kuò)展。
GitEgg-Cloud是一款基于SpringCloud整合搭建的企業(yè)級(jí)微服務(wù)應(yīng)用開發(fā)框架,開源項(xiàng)目地址:

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

歡迎感興趣的小伙伴Star支持一下。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容