HZERO微服務平台11: 代碼分析之數據權限、sql攔截 .md


數據權限實現過程

核心原理

分配功能權限是做加法, 給用戶增加權限; 分配數據權限是做減法, 減少用戶能訪問的數據, 通常是給sql添加過濾條件;
比如查詢用戶:

select * from iam_user as user where .....

限制只能查詢部門1的用戶:

select * from (select * from iam_user where dept_id = 1) as user where .....

把表名iam_user替換成了iam_user的子查詢, 限制了只能查詢到部門1的用戶, 同時沒有影響sql的其他部分;

所以數據權限控制有兩個關鍵步驟:
1.維護規則: 動態維護控制數據權限的規則, 哪些情況下、哪些表要控制權限, 過濾條件是什么等;
2.處理sql: 程序在執行sql前, 對sql進行預處理, 根據某些規則把表名替換成增加了過濾條件的子查詢;

代碼流程

通過代碼的調用流程, 分析hzero是如何實現數據權限的, 是如何實現上述兩個步驟的;
PermissionSqlBuilder#getPermissionRange打斷點;

org.apache.ibatis.plugin.Interceptor //ibatis的插件機制
org.hzero.mybatis.parser.SqlParserInterceptor#intercept
statement = sqlInterceptor.handleStatement(statement....
SqlInterceptor#handleStatement
SqlInterceptor#handlePlainSelect
FromItem afterHandlerFromItem = handleTable((Table) fromItem, serviceName, sqlId, args, userDetails);
PermissionSqlBuilder#handleTable  //①這里把表名替換成了子查詢
PermissionSqlBuilder#handleTable2FromItem
PermissionSqlBuilder#getPermissionRange //②獲取權限規則
PermissionRangeVO permissionRange = this.permissionSqlRepository.getPermissionRange(serviceName, table, sqlId, userDetails.getTenantId());
DefaultPermissionSqlRepository#getPermissionRange
Map<String, String> permissionRangeVOMap = redisHelper.hshGetAll(cacheKey); //從redis里讀取, 初始化是platform服務啟動時完成的;

上述流程的關鍵點:
PermissionSqlBuilder#handleTable: 處理表名
解析sql, 把sql里的表名替換成子查詢, 子查詢實際是mybatis里的xml配置, 由mybatis處理為sql;
xml來自枚舉org.hzero.iam.infra.constant.DocTypeScript;
創建單據權限時, iam服務的DocTypeServiceImpl#createDocType方法獲取xml並替換了變量, 再調用platform的接口插入到hpfm_permission_rule表里;

DefaultPermissionSqlRepository#getPermissionRange: 獲取權限控制規則
數據權限的控制規則來自於redis db1hpfm:permission:{表名}(從這點來看, hzero的所有表不能重名), 其中的"表名"是需要被控制的表; 比如對iam_menu表做權限控制, key是hpfm:permission:iam_menu, value是PermissionRangeVO對象:

{"customRuleFlag":0,"sqlList":[],"dbPrefix":"","rangeExclList":[]}

redis數據的初始化來自platform服務啟動的時候, (所以如果刪除了redis數據, 需要重啟platform服務;) 初始化方法: org.hzero.platform.domain.entity.PermissionRange#initCache(給PermissionRangeVO的構造函數打斷點找到的)

總結一下:

  • 處理sql的關鍵xml來自於枚舉類: DocTypeScript;
  • 對sql做手腳是在PermissionSqlBuilder
  • 數據權限的控制規則來自於: redis db1hpfm:permission:{表名}, platform服務啟動時初始化;

重要的類

SqlInterceptor
org.hzero.mybatis.parser.SqlInterceptor

在Mybatis 攔截器中改寫SQL,實現該接口時按需重寫自己需要改寫SQL的部分即可

SqlParserInterceptor
sqlParser攔截器:

SqlParserInterceptor#sqlInterceptors: 
org.hzero.boot.customize.interceptor.CustomizeSQLInterceptor
org.hzero.boot.platform.data.permission.builder.PermissionSqlBuilder

表名替換為子查詢的xml

過濾條件的xml的示例:

<bind name="roleMergeIdList" value="@io.choerodon.core.oauth.DetailsHelper@getUserDetails().roleMergeIds()" /> 
<bind name="roleAuthHeader" value="@org.hzero.boot.platform.data.permission.util.DocRedisUtils@checkRoleAuthHeaderAssign(121684538047991808L, &quot;BIZ&quot;, roleMergeIdList)" /> 
<bind name="roleAuthLine" value="@org.hzero.boot.platform.data.permission.util.DocRedisUtils@checkRoleAuthLineAssign(121684538047991808L, &quot;BIZ&quot;, &quot;SYS_API_SERVICE&quot;, roleMergeIdList)" /> 
<bind name="userAuthAssign" value="@org.hzero.boot.platform.data.permission.util.DocRedisUtils@checkUserAuthAssign(tenantId, &quot;SYS_API_SERVICE&quot;, userId)" /> 
<choose> 
    <when test="!roleAuthHeader"> 
        1=2 
    </when> 
    <when test="!roleAuthLine"> 
         1=1 
     </when> 
     <when test="!userAuthAssign"> 
         (EXISTS ( 
              SELECT 1  
              FROM hiam_role_auth_data hrad  
              LEFT JOIN hiam_role_auth_data_line hradl ON hrad.auth_data_id = hradl.auth_data_id  
              WHERE hrad.tenant_id = #{tenantId}  
              AND hrad.role_id IN 
             <foreach collection="roleMergeIdList" open="(" separator="," item="roleMergeId" close=")">
                             #{roleMergeId}             
</foreach> 
              AND hrad.authority_type_code = 'SYS_API_SERVICE' 
              AND (hrad.include_all_flag = 1 OR hradl.data_id IN (SELECT hs.service_id FROM
 hadm_service hs
 where   ${tableAlias}.service_name = hs.service_code)))) 
     </when> 
     <when test="userAuthAssign"> 
        (EXISTS ( 
            SELECT 1  
            FROM hiam_user_authority hua1  
            LEFT JOIN hiam_user_authority_line hual1 ON hua1.authority_id = hual1.authority_id  
            WHERE hua1.tenant_id = #{tenantId} 
            AND hua1.user_id = #{userId} 
            AND hua1.authority_type_code = 'SYS_API_SERVICE'  
            AND (hua1.include_all_flag = 1 OR hual1.data_id IN (SELECT hs.service_id FROM
 hadm_service hs
 where   ${tableAlias}.service_name = hs.service_code)))) 
     </when> 
     <otherwise> 
        1=2 
     </otherwise> 
</choose> 

比如: 對iam_permission做權限控制, 當roleAuthHeader等於false時(沒有分配單據權限), 原始sql:

select * from iam_permission ip .....

被替換為:

select * from
    (SELECT
        *
    FROM
        iam_permission DST__0
    WHERE 1=2 ) ip .....

表/實體關系

菜單: 【數據權限規則】、【單據權限】
兩者的關系: 【單據權限】基於【數據權限規則】, 為了便於使用的再次封裝, 創建單據權限實際上自動維護了【數據權限規則】相關的幾張表;

【數據權限規則】

  • hpfm_permission_range 數據屏蔽范圍
    • 規則作用的范圍, 可限定的范圍: 表、服務、sqlid、租戶
  • hpfm_permission_rule 屏蔽規則
    • 現有的規則: 1. 給表加前綴; 2. 單據權限自動生成的
  • hpfm_permission_rel 屏蔽范圍規則關系
    • range和rule的中間表
  • hpfm_permission_range_excl 屏蔽范圍黑名單
    • 現在沒數據

【單據權限】

  • hiam_role_auth_data 角色單據權限管理
    • 包括: 角色、單據權限編碼
  • hiam_role_auth_data_line
    • 包括: 頭id,data_id

數據來源: 【角色管理】-【維護數據權限】

  • hiam_role_auth_data的來源
  • hiam_role_auth_data_line的來源, 比如菜單權限, 新增數據的時候把label_id插入到了hiam_role_auth_data_line.data_id里;

實例: api接口權限、菜單權限

需求:
1.權限集添加權限的時候只能添加本系統的接口;
2.系統管理員只能看到本系統的菜單;

寫sql的步驟:

  • 確定要過濾的表(目標表): api(IAM_PERMISSION)、menu(IAM_MENU)
  • 確定要過濾的表的字段: api.service_name, menu->label
  • 確定字段的取值范圍(值集/值集視圖): hadm_serviceiam_label
  • 確定hiam_role_auth_data_line.data_id要存的字段(只能Long型): hadm_service.service_idiam_label.id
  • 寫sql片段, 查出目標表當前行對應的數據hiam_role_auth_data_line.data_id

實際 api sql

iam_permission替換為:

(
    SELECT
        *
    FROM
        iam_permission DST__0
    WHERE
        (
            EXISTS (
                SELECT
                    1
                FROM
                    hzero_platform.hiam_role_auth_data hrad
                LEFT JOIN hzero_platform.hiam_role_auth_data_line hradl ON hrad.auth_data_id = hradl.auth_data_id
                WHERE
                    hrad.tenant_id = 0
                AND hrad.role_id IN (91468303490486272)
                AND hrad.authority_type_code = 'SYS_API_SERVICE'
                AND (
                    hrad.include_all_flag = 1
                    OR hradl.data_id IN ( /*data_id是值集視圖的valueField*/
                        SELECT
                            hs.service_id
                        FROM
                            hzero_admin.hadm_service hs
                        WHERE
                            DST__0.service_name = hs.service_code
                    )
                )
            )
        )
) ip

實際 menu sql

iam_menu替換為:

(
    SELECT
        *
    FROM
        iam_menu DST__0
    WHERE
        (
            EXISTS (
                SELECT
                    1
                FROM
                    hiam_role_auth_data hrad
                LEFT JOIN hiam_role_auth_data_line hradl ON hrad.auth_data_id = hradl.auth_data_id
                WHERE
                    hrad.tenant_id = 0
                AND hrad.role_id IN (83532216818352128)
                AND hrad.authority_type_code = 'SUBSYS_MENU'
                AND (
                    hrad.include_all_flag = 1
                    OR hradl.data_id IN ( /*data_id是值集視圖的valueField*/
                        SELECT
                            hrl.label_id
                        FROM
                            hiam_label_rel hrl
                        WHERE
                            hrl.data_id = DST__0.id
                        AND hrl.data_type = 'MENU'
                    )
                )
            )
        )
) im

出現問題排查思路

可能出現的問題:

  • 數據權限更新存在bug, 業務范圍、權限數據等要多試幾次才能產生效果;
  • 禁用了單據權限, 但是數據權限還是在控制, sql里生成了1=2

排查思路:
根據數據權限生效的流程來排查:

  • 看日志, 是否報錯; 數據權限攔截器出錯的時候也會查詢出所有數據;
  • 修改界面, 檢查redis、數據庫的數據的變化情況:
  • 檢查redis db1的hpfm:permission:{表名}, 這是過濾規則的直接來源;
  • 檢查數據庫hzero_platform.HPFM_PERMISSION_RANGE, redis的數據來自這里;

其他

  • hzero的數據權限看似靈活強大, 但維護繁瑣、使用困難、容易出現問題;
  • 使用數據權限的前提: 能訪問hzero_platform下的表, 所以需要: 和平台使用同一個數據庫實例, 且有訪問權限;
  • 平台所有服務共用一個redis、一個數據庫實例, 平台相當於分布式單體應用;
  • 角色繼承不能繼承數據權限;
  • hiam_role_auth_data_line.data_id是數字型, 不能存字符串; 所以沒法對字符串過濾, 要先映射/關聯到數字;
  • 如果業務系統要控制數據權限, 如何實現? 推薦思路: 不使用hzero的數據權限功能, 在代碼中根據角色的權限集控制查詢的過濾條件, 硬編碼實現;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM