• No6-6.从零搭建spring-cloud-alibaba微服务框架,添加用户鉴权逻辑,动态数据权限(使用AOP实现)等(六,no6-6)


      代码地址与接口看总目录:【学习笔记】记录冷冷-pig项目的学习过程,大概包括Authorization Server、springcloud、Mybatis Plus~~~_清晨敲代码的博客-CSDN博客

    之前只零碎的学习过spring-cloud-alibaba,并没有全面了解过,这次学习pig框架时,想着可以根据这个项目学习一下,练练手,于是断断续续的用了几天时间搭建了一下基础框架。目前就先重点记录一下遇到的问题吧,毕竟流程也不是特别复杂,就是有的东西没遇到过了解的也不深~

    由于微服务包括认证这里内容太多,所以分了好几篇~

    第一篇文章:No6.从零搭建spring-cloud-alibaba微服务框架,实现fegin、gateway、springevent等(一)_清晨敲代码的博客-CSDN博客

    文章包括:

    1.将服务系统注册到nacos注册中心;

    2.通过nacos实现配置动态更新;

    3.添加fegin服务,实现服务之间调用;

    4.添加网关(学会使用webflux,学会添加过滤器);

    5.添加log服务,通过springevent实现,并使用注解使用(使用AOP);

    第二篇文章:

    No6.从零搭建spring-cloud-alibaba微服务框架,实现数据库调用、用户认证与授权等(二,no6-2)_清晨敲代码的博客-CSDN博客

    文章包括:

    6.添加 mysql 数据库调用,并使用mybatis-plus操作;

    7.在认证模块添加用户认证,基于oauth2的自定义密码模式(已认证用户是基于自定义token加redis持久化,不是session);

    第三篇文章:

    No6-3.从零搭建spring-cloud-alibaba微服务框架,实现资源端用户认证与授权等(三,no6-3)

    8.在资源端模块添加用户认证,基于授权端的认证token添加逻辑(但是没有处理微服务间的不鉴权调用,微服务间的调用接口都是白名单呢!);

    第三篇文章:

    No6-4.从零搭建spring-cloud-alibaba微服务框架,解决微服务间的不鉴权调用等(四,no6-4)

    9.解决微服务间的不鉴权调用(可修改外部访问鉴权逻辑~)

    第三篇文章:

    No6-6.从零搭建spring-cloud-alibaba微服务框架,添加用户鉴权逻辑,操作权限等(六,no6-6)

    10.添加用户鉴权逻辑,操作权限(基于RBAC逻辑,使用springsecuiry安全注解实现)

    本篇内容包括:

    11.添加用户鉴权逻辑,数据权限(使用AOP实现)

    剩余包括(会有变动):

    暂时没有了,后续添加国际化、缓存等内容

    目录

    A11.添加用户鉴权逻辑,数据权限(使用AOP实现)

    B1.业务逻辑分析

    数据权限判断逻辑步骤:

    B2.开始编码

    步骤:

    代码:

    测试:


    A11.添加用户鉴权逻辑,数据权限(使用AOP实现)

    该部分 pig 开源代码里面是没有的,是我自己根据 RouYi 系统 学习的,有兴趣可以去看看~

    B1.业务逻辑分析

    例如:

    1.登录管理员账号后,查看用户列表,可以查看所有的数据;

    2.登录某普通账号后,查询用户列表,会根据他所属的角色里面的数据权限类型进行查询,如果他用户查看操作权限对应的数据权限是仅自己的,就只能查看到自己创建的数据,如果是自定义,就能查看定义下的用户创建的数据;

    首先,数据权限级别分为:全部部门 > 自定义部门 > 当前部门 > 当前部门及子级部门 > 仅自己;也就是说数据权限是根据部门划分的~

    数据权限判断逻辑步骤:

    #1.先获取到用户对应的角色中包含该权限的角色集合,若仅有一个角色,则数据级别就等于该角色的数据权限;若有多个角色则比较数据级别大小,数据级别就等于集合中角色数据权限级别最大的;

    此时就需要给角色和部门添加一个关联表,用于将数据权限是自定义部门情况下的角色绑定自定义部门~

    注意哦,这里有个问题的,如果有多个角色的数据权限是自定义部门,那么就无法去判断获取哪些呀~~~所以必须要有个准确的判断,来保证最终只能拿到一个角色,例如取角色集合自定义情况中的最后一个】

    #2.给查询语句添加级联关系:

    1. SELECT
    2. * //此处为要查询的字段
    3. FROM sys_log //此处为具体要查询内容的表
    4. //下面的一句必须添加,意思是通过创建人关联上用户及用户所属部门
    5. LEFT JOIN sys_user on sys_user.username = sys_log.create_by
    6. //下面的 where 语句根据数据级别进行添加,可以写成活的
    7. where sys_dept.dept_id in (1,3,5)

    #3.根据数据级别进行级联查询:

    (1)全部部门:查询系统中所有的数据,此时就不需要 where 判断语句;

    (2)自定义部门:查询角色绑定的部门集合下的用户创建的数据;

    1. SELECT
    2. *
    3. FROM sys_log
    4. LEFT JOIN sys_user on sys_user.username = sys_log.create_by
    5. -- 此处传角色roleid,用来获取获取角色下的关联的自定义部门
    6. where sys_user.dept_id in
    7. (SELECT sys_role_dept.dept_id
    8. FROM sys_role_dept
    9. where sys_role_dept.role_id = 2)

    (3)当前部门:查询当前用户所在的部门下的用户创建的数据;

    1. SELECT
    2. *
    3. FROM sys_log
    4. LEFT JOIN sys_user on sys_user.username = sys_log.create_by
    5. -- 此处是当前登录账号的dept_id
    6. where sys_user.dept_id = 1

    (4)当前部门及子级部门:查询当前用户所在的部门及其子级部门下的用户创建的数据;

    1. SELECT
    2. *
    3. FROM sys_log
    4. LEFT JOIN sys_user on sys_user.username = sys_log.create_by
    5. -- 此处的两处 3 是当前登录账号的deptid;作用是拿到当前用户所在部门和其所有下级部门
    6. where (sys_user.dept_id in
    7. (SELECT sys_dept.dept_id
    8. FROM sys_dept
    9. where FIND_IN_SET(3,sys_dept.ancestors))
    10. || sys_user.dept_id = 3)

    (5)仅自己 :查询当前用户自己创建的数据;

    1. SELECT
    2. *
    3. FROM sys_log
    4. LEFT JOIN sys_user on sys_user.username = sys_log.create_by
    5. -- 此处是当前登录账号的userid
    6. where sys_user.user_id = 1

    #4.将上面的拼接语句,拼接到mapper.xml文件里面。一个一个手动粘贴太累了,而且如果一旦要再去掉数据权限又特别麻烦,所以我们可以借助 AOP 来实现动态拼接语句,拦截到需要数据鉴权的切点之后,拼接好动态语句,然后放入 mapper.xml 里面进行查询~

    B2.开始编码

    步骤:

    1.添加数据库表角色-部门表 sys_role_dept ,sys_role 表里加 data_scope 字段;

    2.添加角色-部门表实体类,然后增加角色表的属性data_scope,所有涉及到角色这个字段的 mapper 语句,都要修改。

    【由于我们使用 mps 自带的service mapper,所以只要不涉及到具体 sql 就不用修改太多~~~】

    3.修改实体类基类 BaseEntity,添加Map params 属性,用来存储附加信息,例如数据范围sql拼接语句~

    【注意要在 getParams() 方法里面 new 一个 map 对象,否则后面直接 set 是会报错的】

    4. 添加角色-部门表实体类对应的插入逻辑mapper和service,从而实现保存操作权限和数据权限逻辑;在部门controller中添加根据角色查询部门;

    【由于上篇只有添加操作权限的接口逻辑,所以就在接口中直接执行了SysRoleMenuService的方法。现在需要将保存角色-部门逻辑也加入,但又不能直接加入接口中,这样会导致无法执行事务。所以我们将这两个放到SysRoleService类中进行处理~本来他们就是关联逻辑的;

    数据权限和操作权限的保存放一起,是为了方便一起保存后清除用户等缓存~】

    5.添加 @DataScope 注解,添加@DataScope 切面类 PigDataScopeAspect 并拦截 @DataScope 标注,此切面是用来根据当前操作用户拿到数据权限动态 sql 的,其中会添加查询方法~

    【注意这里会有个问题,在切面中,一开始就要根据用户获取角色对应的菜单,由于 Authentication 里面是没有这个数据的,而且 security 模块又没有引入 upms-biz 模块,不能使用 umps 模块的 mapper 获取。所以要么在认证时添加这类数据,要么就在这个模块添加 mapper 文件和接口进行调用。最好是添加到 Authentication 里面。】

    所以,新增一个SysRoleMenuBO 角色-菜单业务类用来存储角色对应的菜单,再在UserInfoVO、PigUser 类中添加属性 List roleMenuBOs,用来存储当前用户对应角色拥有的菜单,在 SysRoleMapper#listRolesMenuByUserIdAndRoleId() 接口中添加方法和对应的sql语句。

    然后在 SysUserServiceImpl#getUserInfo() 方法中拿到 List 并存到 UserInfoVO 中,然后在 PigUserDetailsService 里面添加到 PigUser 对象中,这样就能在 Authentication 对象中获取到了~

    然后在切面类中获取并拿到符合的角色信息~

    6.将 @DataScope 注解标注到需要查询的 service 方法上,将入参类型修改或添加为集成实体基类的类型

    【注意,service方法的入参必须为主查询的实体基类,这样才能够将动态sql插入到 params 属性中,并且必须是第一个参数,当然也可以不是,这里是根据切面里面获取切点参数获取,可以活泛,我这里是写死获取第一个入参的;如果不用基类的属性接收,那么就必须给每个方法加一个入参来接收sql。】;

    7.同时修改 service 方法的 mapper.xml 语句添加拼接用户表,如果使用的是 mps 自带的,那么就需要添加 mapper.xml 语句

    【mps自带的是单表查询,而我们这儿需要多表查询。注意哦,由于五种数据级别中,会涉及到 sys_dept 和 sys_role 表,由于 sys_dept 表是级联数据所以不进行数据权限,由于 sys_user 表在第五种仅查询自己创建的数据时,需要关联 sys_user 表,那么就需要协调好表明字段名~我了解的是通常用户权限相关的操作仅管理员才能操作,所以不会区分数据权限。我们就拿 sys_user 和 sys_role 表举例子~】

    代码:

    接下来就用涉及到的类名以及重点代码代替了,否则代码太多,不想贴(☄ฺ◣ω◢)☄ฺ

    1. //1.添加数据库表角色-部门表 sys_role_dept ,sys_role 表里加 data_scope 字段;
    2. CREATE TABLE `sys_role_dept` (
    3. `role_id` bigint(20) NOT NULL,
    4. `dept_id` bigint(20) NOT NULL,
    5. PRIMARY KEY (`role_id`,`dept_id`)
    6. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='角色数据权限的部门表';
    7. CREATE TABLE `sys_role` (
    8. ...
    9. `data_scope` char(1) DEFAULT NULL COMMENT '数据权限级别分为:0全部部门 > 1自定义部门 > 2当前部门 > 3当前部门及子级部门 > 4仅自己',
    10. ...
    11. PRIMARY KEY (`role_id`),
    12. UNIQUE KEY `role_idx1_role_code` (`role_code`)
    13. ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='系统角色表';
    1. //2.添加角色-部门表实体类,然后增加角色表的属性data_scope,所有涉及到角色这个字段的 mapper 语句,都要修改。
    2. 新建:com.pig4cloud.pig.admin.api.entity.SysRoleDept
    3. 修改:com.pig4cloud.pig.admin.api.entity.SysRole 添加字段 data_scope
    4. 修改:src/main/resources/mapper/SysRoleMapper.xml 在 resultMap 中添加 data_scope,在SQL中添加 data_scope
    5. 修改:src/main/resources/mapper/SysUserMapper.xml 在 resultMap 中添加 data_scope,在SQL中添加 data_scope
    1. //3.修改实体类基类 BaseEntity,添加Map params 属性,用来存储附加信息,例如数据范围sql拼接语句~
    2. 修改:com.pig4cloud.pig.common.mybatis.base.BaseEntity 添加属性
    3. /**
    4. * @Description: 请求附加字段
    5. */
    6. @TableField(exist = false) //一定要添加这个,因为如果使用 mps 自带的操作语句时,会报错;所以要表示当前属性不是数据库的字段
    7. private Map params;
    8. //由于不知道会不会用到 params 属性,所以在 get 时判断并 new 对象,防止浪费
    9. public Map getParams()
    10. {
    11. if (params == null)
    12. {
    13. params = new HashMap<>();
    14. }
    15. return params;
    16. }
    1. //4. 添加角色-部门表实体类对应的插入逻辑mapper和service,从而实现保存操作权限和数据权限逻辑;在部门controller中添加根据角色查询部门;
    2. 修改:com.pig4cloud.pig.admin.api.vo.RoleVO,使他继承 SysRole ,方便后续调用;再添加属性 String deptIds;
    3. 新建:com.pig4cloud.pig.admin.mapper.SysRoleDeptMapper ,继承 mps 提供的 BaseMapper
    4. 修改:com.pig4cloud.pig.admin.service.SysRoleService ,添加 updateMenusAndDataScope() 方法,在里面处理操作权限、数据权限、数据范围的修改;
    5. 原来的 com.pig4cloud.pig.admin.service.impl.SysRoleMenuServiceImpl#saveRoleMenus() 方法就可以弃用了。
    1. //5.添加 @DataScope 注解,添加@DataScope 切面类 PigDataScopeAspect 并拦截 @DataScope 标注,此切面是用来根据当前操作用户拿到数据权限动态 sql 的,其中会添加查询方法~
    2. //(1)先来在 Authentication 中添加角色对应菜单的列表
    3. 修改:com.pig4cloud.pig.admin.api.vo.UserVO 添加属性List roleList;
    4. 修改:com.pig4cloud.pig.common.security.service.PigUser 添加属性List roleList;
    5. 修改:com.pig4cloud.pig.admin.api.vo.UserInfoVO 里面添加属性createBy updateBy ,之前忘了加;
    6. 修改:com.pig4cloud.pig.admin.mapper.SysRoleMapper 里面添加 listRolesMenuByUserIdAndRoleId() 方法,通过用户ID和菜单权限,查询匹配中的角色-菜单信息,用于数据鉴权里面
    7. 修改:com.pig4cloud.pig.admin.service.impl.SysUserServiceImpl#getUserInfo 在方法里面调用SysRoleMapper#listRolesMenuByUserIdAndRoleId 并保存到 UserInfoVO#roleMenuBOs 里面
    8. 修改:com.pig4cloud.pig.common.security.service.PigUserDetailsService#getUserDetails 方法,在return 的 PigUser 里面添加 UserInfoVO#roleMenuBOs 属性
    9. //这样我们就在 Authentication 里面拿到角色-部门信息了
    10. //(2)编写 @DataScope 注解和切面类,以及对应的mapper
    11. 新建:com.pig4cloud.pig.common.security.annotation.DataScope 用来标注到需要数据权限的service查询方法上
    12. 新建:com.pig4cloud.pig.common.security.aspect.PigDataScopeAspect 直接看带代码能理解啦
    1. @Slf4j
    2. @Aspect
    3. @RequiredArgsConstructor
    4. public class PigDataScopeAspect implements Ordered {
    5. /**
    6. * 数据权限过滤关键字
    7. */
    8. public static final String DATA_SCOPE = "dataScope";
    9. @SneakyThrows
    10. @Before("@annotation(dataScope)")
    11. public void around(JoinPoint point, DataScope dataScope){
    12. //拿到当前用户
    13. PigUser pigUser = SecurityUtils.getUser();
    14. //从 dataScope 中拿到当前操作的唯一标识
    15. String permission = dataScope.permission();
    16. //从当前用户认证信息中拿到角色-菜单列表,然后匹配中permission,然后直接拿第一个值就可以~【因为查询时已经排序了】
    17. List roleMenuBOList = SecurityUtils.getUser().getRoleMenuBOs()
    18. .stream()
    19. .filter(roleMenuBO-> permission.equals(roleMenuBO.getPermission()))
    20. .collect(Collectors.toList());
    21. //直接拿到角色集合里面的第一个角色id和他的数据权限类型
    22. SysRoleMenuBO sysRoleMenuBO = roleMenuBOList.get(0);
    23. Long roleId = sysRoleMenuBO.getRoleId();
    24. String dataScopeValue = sysRoleMenuBO.getDataScope();
    25. //拼接动态语句
    26. this.joinDataScope(point, pigUser, roleId, dataScopeValue);
    27. }
    28. /**
    29. * @Description: 拼接动态语句
    30. * @param pigUser
    31. * @param roleId
    32. * @param dataScopeValue
    33. * @Return: void
    34. */
    35. private void joinDataScope(JoinPoint point, PigUser pigUser, Long roleId, String dataScopeValue) {
    36. StringBuilder sqlString = new StringBuilder();
    37. //(1).该角色数据权限类型是:0全部部门
    38. if(dataScopeValue.equals(SecurityConstants.DATA_SCOPE_ALL)){
    39. sqlString.append(" 1 = 1 ");
    40. }//(2).该角色数据权限类型是:1自定义部门
    41. else if (dataScopeValue.equals(SecurityConstants.DATA_SCOPE_CUSTOM)){
    42. sqlString.append(StrUtil.format(" sys_user.dept_id in (SELECT sys_role_dept.dept_id FROM sys_role_dept where sys_role_dept.role_id = {})",roleId));
    43. }//(3).该角色数据权限类型是:2当前部门
    44. else if (dataScopeValue.equals(SecurityConstants.DATA_SCOPE_DEPT)){
    45. sqlString.append(StrUtil.format(" sys_user.dept_id = {} ",pigUser.getDeptId()));
    46. }//(4).该角色数据权限类型是:3当前部门及子级部门
    47. else if (dataScopeValue.equals(SecurityConstants.DATA_SCOPE_DEPT_AND_CHILD)){
    48. sqlString.append(StrUtil.format(" (sys_user.dept_id in FROM (sys_dept where FIND_IN_SET({},sys_dept.ancestors)) || sys_user.dept_id = {}) ", pigUser.getDeptId(), pigUser.getDeptId()));
    49. }//(5).该角色数据权限类型是:4仅自己
    50. else if (dataScopeValue.equals(SecurityConstants.DATA_SCOPE_SELF)){
    51. sqlString.append(StrUtil.format(" sys_user.user_id = '{}' ", pigUser.getId()));
    52. }
    53. if (StringUtils.hasText(sqlString.toString())) {
    54. //获取到当前请求 service 代理类的方法的某个请求参数,例如:selectUserList(SysUser user),这个的getArgs()[0]就是 SysUser
    55. Object params = point.getArgs()[0];
    56. if (params != null && params instanceof BaseEntity)
    57. {
    58. //向 SysUser 参数中的父类属性 params 添加数据,会在 mapper 层使用
    59. BaseEntity baseEntity = (BaseEntity) params;
    60. baseEntity.getParams().put(DATA_SCOPE, sqlString);
    61. }
    62. }
    63. }
    64. @Override
    65. public int getOrder() {
    66. return Ordered.HIGHEST_PRECEDENCE + 1;
    67. }
    68. }
    1. //6.将 @DataScope 注解标注到需要查询的 service 方法上,将入参类型修改或添加为集成实体基类的类型
    2. //拿查询用户列表的接口举例子
    3. 修改:com.pig4cloud.pig.admin.service.impl.SysUserServiceImpl#getUserWithRolePage 修改入参,并且添加注解,注解里的 permission 值要和权限的标识匹配上啊,我们这里用的是菜单表里面类型为菜单的权限标识
    1. //7.同时修改 service 方法的 mapper.xml 语句添加拼接用户表,如果使用的是 mps 自带的,那么就需要添加 mapper.xml 语句
    2. 修改:src/main/resources/mapper/SysUserMapper.xml
    1. <select id="getUserVosPage" resultMap="baseResultMap">
    2. SELECT
    3. suser.user_id,
    4. suser.username,
    5. suser.salt,
    6. suser.phone,
    7. suser.avatar,
    8. suser.dept_id,
    9. suser.create_time AS ucreate_time,
    10. suser.update_time AS uupdate_time,
    11. suser.create_by AS ucreate_by,
    12. suser.update_by AS uupdate_by,
    13. suser.del_flag AS udel_flag,
    14. suser.lock_flag AS lock_flag,
    15. suser.dept_id AS deptId,
    16. sys_dept.dept_name AS deptName
    17. FROM sys_user suser
    18. LEFT JOIN sys_dept ON sys_dept.dept_id = suser.dept_id
    19. LEFT JOIN sys_user ON sys_user.username = suser.create_by
    20. <where>
    21. suser.del_flag = '0'
    22. <if test="query.username != null and query.username != ''">
    23. <bind name="usernameLike" value="'%' + query.username + '%'" />
    24. and suser.username LIKE #{usernameLike}
    25. if>
    26. and ${query.params.dataScope}
    27. where>
    28. ORDER BY suser.create_time DESC
    29. select>

    测试:

     测试的时候记得对照这数据库测试,可以先查出用户所有的角色对应的菜单,这样好对比~

    那么当前用户的查询用户的数据权限范围就是 2 ,可以看到 mapper 里面的拼接语句就是按照当前部门查询的 

  • 相关阅读:
    验证码案例 —— Kaptcha 插件介绍 后端生成验证码,前端展示并进行session验证(带完整前后端源码)
    【西瓜书】5.神经网络
    信奥中的数学:加法原理和乘法原理
    FreeRTOS之列表
    手把手操作JS逆向爬虫入门(三)---Headers请求头参数加密
    前端进阶特训营-TDD制造rollup-0
    实验二:数据类型、运算符和表达式——桂林航天工业学院
    基于Qt 的CAN Bus实现
    c++运算符
    【知识网络分析】 一模网络(one node)
  • 原文地址:https://blog.csdn.net/vaevaevae233/article/details/127653783