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.给查询语句添加级联关系:

SELECT 
*                     //此处为要查询的字段
FROM sys_log          //此处为具体要查询内容的表

//下面的一句必须添加,意思是通过创建人关联上用户及用户所属部门
LEFT JOIN sys_user on sys_user.username = sys_log.create_by

//下面的 where 语句根据数据级别进行添加,可以写成活的
where sys_dept.dept_id in (1,3,5)

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

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

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

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

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

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

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

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

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

SELECT 
*                     
FROM sys_log          
LEFT JOIN sys_user on sys_user.username = sys_log.create_by
-- 此处是当前登录账号的userid
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<String, Object> 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<SysRoleMenuBO> roleMenuBOs,用来存储当前用户对应角色拥有的菜单,在 SysRoleMapper#listRolesMenuByUserIdAndRoleId() 接口中添加方法和对应的sql语句。

然后在 SysUserServiceImpl#getUserInfo() 方法中拿到 List<SysRoleMenuBO> 并存到 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.添加数据库表角色-部门表 sys_role_dept ,sys_role 表里加 data_scope 字段;

CREATE TABLE `sys_role_dept` (
  `role_id` bigint(20) NOT NULL,
  `dept_id` bigint(20) NOT NULL,
  PRIMARY KEY (`role_id`,`dept_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='角色数据权限的部门表';

CREATE TABLE `sys_role` (
...
  `data_scope` char(1) DEFAULT NULL COMMENT '数据权限级别分为:0全部部门 > 1自定义部门 > 2当前部门 > 3当前部门及子级部门 > 4仅自己',
...
  PRIMARY KEY (`role_id`),
  UNIQUE KEY `role_idx1_role_code` (`role_code`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='系统角色表';
//2.添加角色-部门表实体类,然后增加角色表的属性data_scope,所有涉及到角色这个字段的 mapper 语句,都要修改。

新建:com.pig4cloud.pig.admin.api.entity.SysRoleDept

修改:com.pig4cloud.pig.admin.api.entity.SysRole 添加字段 data_scope

修改:src/main/resources/mapper/SysRoleMapper.xml 在 resultMap 中添加 data_scope,在SQL中添加 data_scope 

修改:src/main/resources/mapper/SysUserMapper.xml 在 resultMap 中添加 data_scope,在SQL中添加 data_scope 
//3.修改实体类基类 BaseEntity,添加Map<String, Object> params 属性,用来存储附加信息,例如数据范围sql拼接语句~

修改:com.pig4cloud.pig.common.mybatis.base.BaseEntity 添加属性

    /**
     * @Description: 请求附加字段
     */
    @TableField(exist = false)     //一定要添加这个,因为如果使用 mps 自带的操作语句时,会报错;所以要表示当前属性不是数据库的字段
    private Map<String, Object> params;

    //由于不知道会不会用到 params 属性,所以在 get 时判断并 new 对象,防止浪费
    public Map<String, Object> getParams()
    {
        if (params == null)
        {
            params = new HashMap<>();
        }
        return params;
    }
//4. 添加角色-部门表实体类对应的插入逻辑mapper和service,从而实现保存操作权限和数据权限逻辑;在部门controller中添加根据角色查询部门;

修改:com.pig4cloud.pig.admin.api.vo.RoleVO,使他继承 SysRole ,方便后续调用;再添加属性  String deptIds;

新建:com.pig4cloud.pig.admin.mapper.SysRoleDeptMapper ,继承 mps 提供的 BaseMapper

修改:com.pig4cloud.pig.admin.service.SysRoleService ,添加 updateMenusAndDataScope() 方法,在里面处理操作权限、数据权限、数据范围的修改;
原来的 com.pig4cloud.pig.admin.service.impl.SysRoleMenuServiceImpl#saveRoleMenus() 方法就可以弃用了。

//5.添加 @DataScope 注解,添加@DataScope 切面类 PigDataScopeAspect 并拦截 @DataScope 标注,此切面是用来根据当前操作用户拿到数据权限动态 sql 的,其中会添加查询方法~
//(1)先来在 Authentication 中添加角色对应菜单的列表

修改:com.pig4cloud.pig.admin.api.vo.UserVO 添加属性List<SysRole> roleList;

修改:com.pig4cloud.pig.common.security.service.PigUser 添加属性List<SysRole> roleList;

修改:com.pig4cloud.pig.admin.api.vo.UserInfoVO 里面添加属性createBy updateBy ,之前忘了加;

修改:com.pig4cloud.pig.admin.mapper.SysRoleMapper 里面添加 listRolesMenuByUserIdAndRoleId() 方法,通过用户ID和菜单权限,查询匹配中的角色-菜单信息,用于数据鉴权里面

修改:com.pig4cloud.pig.admin.service.impl.SysUserServiceImpl#getUserInfo 在方法里面调用SysRoleMapper#listRolesMenuByUserIdAndRoleId 并保存到 UserInfoVO#roleMenuBOs 里面

修改:com.pig4cloud.pig.common.security.service.PigUserDetailsService#getUserDetails 方法,在return 的 PigUser 里面添加 UserInfoVO#roleMenuBOs 属性


//这样我们就在 Authentication 里面拿到角色-部门信息了

//(2)编写 @DataScope 注解和切面类,以及对应的mapper

新建:com.pig4cloud.pig.common.security.annotation.DataScope 用来标注到需要数据权限的service查询方法上

新建:com.pig4cloud.pig.common.security.aspect.PigDataScopeAspect 直接看带代码能理解啦

@Slf4j
@Aspect
@RequiredArgsConstructor
public class PigDataScopeAspect implements Ordered {

    /**
     * 数据权限过滤关键字
     */
    public static final String DATA_SCOPE = "dataScope";

    @SneakyThrows
    @Before("@annotation(dataScope)")
    public void around(JoinPoint point, DataScope dataScope){

        //拿到当前用户
        PigUser pigUser = SecurityUtils.getUser();

        //从 dataScope 中拿到当前操作的唯一标识
        String permission = dataScope.permission();

        //从当前用户认证信息中拿到角色-菜单列表,然后匹配中permission,然后直接拿第一个值就可以~【因为查询时已经排序了】
        List<SysRoleMenuBO> roleMenuBOList = SecurityUtils.getUser().getRoleMenuBOs()
                .stream()
                .filter(roleMenuBO-> permission.equals(roleMenuBO.getPermission()))
                .collect(Collectors.toList());

        //直接拿到角色集合里面的第一个角色id和他的数据权限类型
        SysRoleMenuBO sysRoleMenuBO = roleMenuBOList.get(0);
        Long roleId = sysRoleMenuBO.getRoleId();
        String dataScopeValue = sysRoleMenuBO.getDataScope();

        //拼接动态语句
        this.joinDataScope(point, pigUser, roleId, dataScopeValue);
    }

    /**
     * @Description: 拼接动态语句
     * @param pigUser
     * @param roleId
     * @param dataScopeValue
     * @Return: void
     */
    private void joinDataScope(JoinPoint point, PigUser pigUser, Long roleId, String dataScopeValue) {

        StringBuilder sqlString = new StringBuilder();

        //(1).该角色数据权限类型是:0全部部门
        if(dataScopeValue.equals(SecurityConstants.DATA_SCOPE_ALL)){
            sqlString.append(" 1 = 1 ");
        }//(2).该角色数据权限类型是:1自定义部门
        else if (dataScopeValue.equals(SecurityConstants.DATA_SCOPE_CUSTOM)){
            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));

        }//(3).该角色数据权限类型是:2当前部门
        else if (dataScopeValue.equals(SecurityConstants.DATA_SCOPE_DEPT)){
            sqlString.append(StrUtil.format(" sys_user.dept_id = {} ",pigUser.getDeptId()));

        }//(4).该角色数据权限类型是:3当前部门及子级部门
        else if (dataScopeValue.equals(SecurityConstants.DATA_SCOPE_DEPT_AND_CHILD)){
            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()));

        }//(5).该角色数据权限类型是:4仅自己
        else if (dataScopeValue.equals(SecurityConstants.DATA_SCOPE_SELF)){
            sqlString.append(StrUtil.format(" sys_user.user_id = '{}' ", pigUser.getId()));

        }

        if (StringUtils.hasText(sqlString.toString())) {
            //获取到当前请求 service 代理类的方法的某个请求参数,例如:selectUserList(SysUser user),这个的getArgs()[0]就是 SysUser
            Object params = point.getArgs()[0];

            if (params != null && params instanceof BaseEntity)
            {
                //向 SysUser 参数中的父类属性 params 添加数据,会在 mapper 层使用
                BaseEntity baseEntity = (BaseEntity) params;
                baseEntity.getParams().put(DATA_SCOPE, sqlString);
            }

        }
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 1;
    }

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

//拿查询用户列表的接口举例子
修改:com.pig4cloud.pig.admin.service.impl.SysUserServiceImpl#getUserWithRolePage 修改入参,并且添加注解,注解里的 permission 值要和权限的标识匹配上啊,我们这里用的是菜单表里面类型为菜单的权限标识
//7.同时修改 service 方法的 mapper.xml 语句添加拼接用户表,如果使用的是 mps 自带的,那么就需要添加 mapper.xml 语句


修改:src/main/resources/mapper/SysUserMapper.xml 
<!-- 这里记得也要 LEFT JOIN sys_user -->
    <select id="getUserVosPage" resultMap="baseResultMap">
        SELECT
        suser.user_id,
        suser.username,
        suser.salt,
        suser.phone,
        suser.avatar,
        suser.dept_id,
        suser.create_time AS ucreate_time,
        suser.update_time AS uupdate_time,
        suser.create_by AS ucreate_by,
        suser.update_by AS uupdate_by,
        suser.del_flag AS udel_flag,
        suser.lock_flag AS lock_flag,
        suser.dept_id AS deptId,
        sys_dept.dept_name AS deptName
        FROM sys_user suser
        LEFT JOIN sys_dept ON sys_dept.dept_id = suser.dept_id
        LEFT JOIN sys_user  ON sys_user.username = suser.create_by
        <where>
            suser.del_flag = '0'
            <if test="query.username != null and query.username != ''">
                <bind name="usernameLike" value="'%' + query.username + '%'" />
                and suser.username LIKE  #{usernameLike}
            </if>
            <!-- 数据范围过滤 -->
            and ${query.params.dataScope}
        </where>
        ORDER BY suser.create_time DESC
    </select>

测试:

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值