SpringCloud微服務實戰——搭建企業級開發框架(二十八):擴充套件MybatisPlus外掛DataPermissionInterceptor實現資料許可權控制

全棧程式猿發表於2021-12-03

一套完整的系統許可權需要支援功能許可權和資料許可權,前面介紹了系統通過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);列級資料許可權除了需要配置上面提到的之外,還需要配置可以訪問的欄位或者排除訪問的欄位。

資料許可權

在資源管理配置資源關聯介面的資料許可權規則(t_sys_data_permission_role),通過RBAC的方式用角色和使用者關聯,在使用者管理配置使用者同角色的多個部門資料許可權,使用者直接和部門關聯(t_sys_data_permission_user)。系統資料許可權管理功能設計如下所示:

許可權管理

資料許可權表設計:
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);
    }

}
  • 自定義實現DataPermissionHandler資料許可權控制
@Component
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class GitEggDataPermissionHandler implements DataPermissionHandler {

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

    /**
     * 註解方式預設關閉,這裡只是說明一種實現方式,實際使用時,使用配置的方式即可
     */
    @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 當有資料許可權配置時才去判斷使用者是否有資料許可權控制
            if (ObjectUtils.isNotEmpty(loginUser) && CollectionUtils.isNotEmpty(loginUser.getDataPermissionTypeList())) {
                // 1 根據系統配置的資料許可權拼裝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是否有配置資料許可權
                if (ObjectUtils.isNotEmpty(dataPermissionEntity))
                {
                    dataPermissionFilter(loginUser, dataPermissionEntity, plainSelect);
                }
                //預設不開啟註解,因每次查詢都遍歷註解,影響效能,直接選擇使用配置的方式實現資料許可權即可
                else if(annotationEnable)
                {
                    // 2 根據註解的資料許可權拼裝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) {
                        //當有多個時,這個方法可以獲取到
                        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();
        }
    }

    /**
     * 構建過濾條件
     *
     * @param user 當前登入使用者
     * @param plainSelect plainSelect
     * @return 構建後查詢條件
     */
    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();

        // 列級資料許可權
        String dataColumnExclude = dataPermissionEntity.getDataColumnExclude();
        String dataColumnInclude = dataPermissionEntity.getDataColumnInclude();
        List<string> includeColumns = new ArrayList<>();
        List<string> excludeColumns = new ArrayList<>();
        // 只包含這幾個欄位,也就是不是這幾個欄位的,直接刪除
        if (StringUtils.isNotEmpty(dataColumnInclude))
        {
            includeColumns = Arrays.asList(dataColumnInclude.split(StringPool.COMMA));
        }

        // 需要排除這幾個欄位
        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);
            }
        }

        // 行級資料許可權
        // 查詢使用者機構和子機構的資料,這裡是使用where條件新增子查詢的方式來實現的,這樣的實現方式好處是不需要判斷Update,Insert還是Select,都是通用的,缺點是效能問題。
        if (DataPermissionTypeEnum.DATA_PERMISSION_ORG_AND_CHILD.getLevel().equals(dataPermissionType)) {
            // 如果是table的話,那麼直接加inner,如果不是,那麼直接在where條件里加子查詢
            if (plainSelect.getFromItem() instanceof Table)
            {
                Table fromTable = (Table)plainSelect.getFromItem();
                //資料主表
                Table dataTable = null;
                //inner資料許可權表
                Table innerTable = null;
                if (fromTable.getName().equalsIgnoreCase(dataTableName))
                {
                    dataTable = (Table)plainSelect.getFromItem();
                }

                // 如果是查詢,這裡使用inner join關聯過濾,不使用子查詢,因為join不需要建立臨時表,因此速度比子查詢快。
                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;
                            }
                        }
                    }
                }

                //如果沒有找到資料主表,那麼直接丟擲異常
                if (null == dataTable)
                {
                    throw new BusinessException("在SQL語句中沒有找到資料許可權配置的主表,資料許可權過濾失敗。");
                }

                //如果不存在這個table,那麼新增一個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);
                //判斷是否有資料許可權,如果有資料許可權配置,那麼新增資料許可權的機構列表
                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);

                //判斷是否有資料許可權,如果有資料許可權配置,那麼新增資料許可權的機構列表
                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);
            }
        }
        // 只查詢使用者擁有機構的資料,不包含子機構
        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);

        }
        // 只能查詢個人資料
        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);
        }
        //當型別為檢視所有資料時,不處理
//        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("沒有配置自定義表示式");
            }
            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("自定義表示式配置錯誤");
            }
        }
    }

    /**
     * 構建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);
    }


    /**
     * 註解轉為實體類
     * @param annotation 註解
     * @return 實體類
     */
    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;
    }
  • 系統啟動時初始化資料許可權配置到Redis
    @Override
    public void initDataRolePermissions() {
        List<datapermissionroledto> dataPermissionRoleList = dataPermissionRoleMapper.queryDataPermissionRoleListAll();
        // 判斷是否開啟了租戶模式,如果開啟了,那麼角色許可權需要按租戶進行分類儲存
        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);
    }
資料許可權配置指南:

image.png

  • 資料許可權名稱:自定義一個名稱,方便查詢和區分
  • Mapper全路徑: Mapper路徑配置到具體方法名稱,例:com.gitegg.service.system.mapper.UserMapper.selectUserList
  • 資料許可權型別:
    只能檢視本人(實現原理是在查詢條件新增資料表的creator條件)
    只能檢視本部門 (實現原理是在查詢條件新增資料表的部門條件)
    只能檢視本部門及子部門 (實現原理是在查詢條件新增資料表的部門條件)
    可以檢視所有資料(不處理)
    自定義(新增where子條件)
註解配置資料許可權配置指南:
    /**
     * 查詢使用者列表
     * @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);
行級資料許可權配置:

資料主表:主資料表,用於資料操作時的主表,例如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

相關文章