• 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的数据权限功能, 在代码中根据角色的权限集控制查询的过滤条件, 硬编码实现;
  • 相关阅读:
    第四次作业
    angular2 安装 打包成发布项目过程
    关于cannot find module 'xxxx’的一个可能解决方法。
    对cordova插件配置文件plugin.xml的理解
    如何找某个样式属于哪个Element
    如何用plugman编辑和添加cordova插件
    python命令行使用的问题
    如何使用Sencha touch 构建基于Cordova的安卓项目
    tensorflow1.0.0 弃用了几个operator写法
    Asp.net(C#)给图片加上水印效果和按比例生成高质量缩略图
  • 原文地址:https://www.cnblogs.com/QIAOXINGXING001/p/15631331.html
Copyright © 2020-2023  润新知