写点什么

【HZERO 微服务平台 6】源码分析之数据权限、sql 拦截

作者:qiaoxingxing
  • 2021 年 12 月 14 日
  • 本文字数:4749 字

    阅读完需:约 16 分钟

数据权限实现过程

核心原理

分配功能权限是做加法, 给用户增加权限; 分配数据权限是做减法, 减少用户能访问的数据, 通常是给 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#interceptstatement = sqlInterceptor.handleStatement(statement....SqlInterceptor#handleStatementSqlInterceptor#handlePlainSelectFromItem afterHandlerFromItem = handleTable((Table) fromItem, serviceName, sqlId, args, userDetails);PermissionSqlBuilder#handleTable  //①这里把表名替换成了子查询PermissionSqlBuilder#handleTable2FromItemPermissionSqlBuilder#getPermissionRange //②获取权限规则PermissionRangeVO permissionRange = this.permissionSqlRepository.getPermissionRange(serviceName, table, sqlId, userDetails.getTenantId());DefaultPermissionSqlRepository#getPermissionRangeMap<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 db1 hpfm: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 db1 hpfm:permission:{表名}, platform 服务启动时初始化;

重要的类

SqlInterceptororg.hzero.mybatis.parser.SqlInterceptor


在 Mybatis 拦截器中改写 SQL,实现该接口时按需重写自己需要改写 SQL 的部分即可


SqlParserInterceptorsqlParser 拦截器:


SqlParserInterceptor#sqlInterceptors: org.hzero.boot.customize.interceptor.CustomizeSQLInterceptororg.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 的数据权限功能, 在代码中根据角色的权限集控制查询的过滤条件, 硬编码实现;

用户头像

qiaoxingxing

关注

还未添加个人签名 2021.12.07 加入

还未添加个人简介

评论

发布
暂无评论
【HZERO微服务平台6】源码分析之数据权限、sql拦截