摸索若依框架是如何实现权限过滤的

摸索若依框架是如何实现权限过滤的

这篇文章,我也是作为一个优秀开源框架的学习者,在这里摸索这套框架是如何实现权限过滤的,这个封装对于入行Java半年之余的我来说,理解起来有些困难,所以,文章只是作为一个学习记录,如果有前辈指正,请不要多加指责,敬请提出宝贵意见,学生一定虚心学习请教,感谢!!!手下留情,不要大家难堪!

在这里插入图片描述

首先,简单的看一下若依明显的几个地方,首先就是service层留下了一个自定义注解

    @Override@DataScope(deptAlias = "d", userAlias = "u")public List<SysUser> selectUserList(SysUser user){return userMapper.selectUserList(user);}

@DataScope(deptAlias = "d", userAlias = "u") 是一个 Java 注解的用法,用于标注在方法上。这个注解属于自定义注解,它是根据 DataScope 类的定义创建的。

/*** 数据权限过滤注解* * @author ruoyi*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope
{/*** 部门表的别名*/public String deptAlias() default "";/*** 用户表的别名*/public String userAlias() default "";/*** 权限字符(用于多个角色匹配符合要求的权限)默认根据权限注解@ss获取,多个权限用逗号分隔开来*/public String permission() default "";
}

在这里,通过提供属性值为 deptAliasuserAlias 的方式对 DataScope 进行了配置。

具体来说:

  • @DataScope 是一个自定义注解,它定义了一些属性,包括 deptAliasuserAlias
  • deptAliasuserAlias@DataScope 注解的属性,它们有默认值,但在这里可以通过具体的值进行了配置。

这样的注解的目的通常是为了在标注了这个注解的方法中,通过 AOP 切面提供的机制来处理特定的逻辑。在代码中,DataScopeAspect 切面会根据这个注解的配置对数据进行范围过滤,确保用户只能访问其具有权限查看的数据。同时呢,我们留意一下 deptAliasuserAlias,这个两个字段分部对应的就是sys_dept 这个表和sys_user。我们慢慢往后看——

然后呢,我们进入mapper,看一下SQL是什么样子

 <select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult">select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader from sys_user uleft join sys_dept d on u.dept_id = d.dept_idwhere u.del_flag = '0'<if test="userId != null and userId != 0">AND u.user_id = #{userId}</if><if test="userName != null and userName != ''">AND u.user_name like concat('%', #{userName}, '%')</if><if test="status != null and status != ''">AND u.status = #{status}</if><if test="phonenumber != null and phonenumber != ''">AND u.phonenumber like concat('%', #{phonenumber}, '%')</if><if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->AND date_format(u.create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')</if><if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->AND date_format(u.create_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')</if><if test="deptId != null and deptId != 0">AND (u.dept_id = #{deptId} OR u.dept_id IN ( SELECT t.dept_id FROM sys_dept t WHERE find_in_set(#{deptId}, ancestors) ))</if><!-- 数据范围过滤 -->${params.dataScope}</select>

这里,我们可以看到,${params.dataScope}

在MyBatis XML映射文件中,${params.dataScope} 是一个动态 SQL 片段,它会被实际的数据范围过滤逻辑替换。这个 SQL 片段是在运行时动态生成的,它会被注入到你的 SQL 查询中。

DataScopeAspect 切面类中的 dataScopeFilter 方法会设置 DATA_SCOPE 参数,这个参数会被添加到 BaseEntity 的参数集合中。在这个场景中,params 就是 BaseEntitydataScope 参数将被放置在 params 的参数集合中。

在你的 SQL 查询中,${params.dataScope} 将被实际的数据范围过滤条件替换。这个过滤条件是根据用户的角色和权限动态生成的,确保只返回用户有权限访问的数据。

@DataScope(deptAlias = "d", userAlias = "u") 注解相关的内容在 DataScopeAspect 切面中,这个切面负责根据用户的数据权限进行过滤。所以,${params.dataScope} 的具体内容将由 DataScopeAspect 动态生成,确保只返回用户有权限访问的数据。

现在,我们逐渐提高了更多的东西,首先就是DataScopeAspect 这个切面类,然后还有一个参数集合 BaseEntity

我们先不深入,我们先看看 BaseEntity ,其实这个倒是无所谓,主要就是params会被填入参数,所以就大概看一眼

/*** Entity基类* * @author ruoyi*/
public class BaseEntity implements Serializable
{private static final long serialVersionUID = 1L;/** 搜索值 */@JsonIgnoreprivate String searchValue;/** 创建者 */private String createBy;/** 创建时间 */@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private Date createTime;/** 更新者 */private String updateBy;/** 更新时间 */@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private Date updateTime;/** 备注 */private String remark;/** 请求参数 */@JsonInclude(JsonInclude.Include.NON_EMPTY)private Map<String, Object> params;......此处省去get/set方法 
}

可以看到,大概如此罢了,然而这里有一个params会涉及到SQL安全问题,有可能造成SQL注入,所以在后面代码过程中,我们可以看到若依对于SQL注入的处理方法。

下来,就是这个切面类,这里的学问比较多,也是核心,我们需要静下心慢慢看

package com.ruoyi.framework.aspectj;import java.util.ArrayList;
import java.util.List;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import com.ruoyi.common.annotation.DataScope;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.context.PermissionContextHolder;/*** 数据过滤处理** @author ruoyi*/
@Aspect
@Component
public class DataScopeAspect
{/*** 全部数据权限*/public static final String DATA_SCOPE_ALL = "1";/*** 自定数据权限*/public static final String DATA_SCOPE_CUSTOM = "2";/*** 部门数据权限*/public static final String DATA_SCOPE_DEPT = "3";/*** 部门及以下数据权限*/public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";/*** 仅本人数据权限*/public static final String DATA_SCOPE_SELF = "5";/*** 数据权限过滤关键字*/public static final String DATA_SCOPE = "dataScope";@Before("@annotation(controllerDataScope)")public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable{clearDataScope(point);handleDataScope(point, controllerDataScope);}protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope){// 获取当前的用户LoginUser loginUser = SecurityUtils.getLoginUser();if (StringUtils.isNotNull(loginUser)){SysUser currentUser = loginUser.getUser();// 如果是超级管理员,则不过滤数据if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin()){String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(), PermissionContextHolder.getContext());dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),controllerDataScope.userAlias(), permission);}}}/*** 数据范围过滤** @param joinPoint 切点* @param user 用户* @param deptAlias 部门别名* @param userAlias 用户别名* @param permission 权限字符*/public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, String permission){StringBuilder sqlString = new StringBuilder();List<String> conditions = new ArrayList<String>();for (SysRole role : user.getRoles()){String dataScope = role.getDataScope();if (!DATA_SCOPE_CUSTOM.equals(dataScope) && conditions.contains(dataScope)){continue;}if (StringUtils.isNotEmpty(permission) && StringUtils.isNotEmpty(role.getPermissions())&& !StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission))){continue;}if (DATA_SCOPE_ALL.equals(dataScope)){sqlString = new StringBuilder();conditions.add(dataScope);break;}else if (DATA_SCOPE_CUSTOM.equals(dataScope)){sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,role.getRoleId()));}else if (DATA_SCOPE_DEPT.equals(dataScope)){sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));}else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)){sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",deptAlias, user.getDeptId(), user.getDeptId()));}else if (DATA_SCOPE_SELF.equals(dataScope)){if (StringUtils.isNotBlank(userAlias)){sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));}else{// 数据权限为仅本人且没有userAlias别名不查询任何数据sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));}}conditions.add(dataScope);}// 多角色情况下,所有角色都不包含传递过来的权限字符,这个时候sqlString也会为空,所以要限制一下,不查询任何数据if (StringUtils.isEmpty(conditions)){sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));}if (StringUtils.isNotBlank(sqlString.toString())){Object params = joinPoint.getArgs()[0];if (StringUtils.isNotNull(params) && params instanceof BaseEntity){BaseEntity baseEntity = (BaseEntity) params;baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");}}}/*** 拼接权限sql前先清空params.dataScope参数防止注入*/private void clearDataScope(final JoinPoint joinPoint){Object params = joinPoint.getArgs()[0];if (StringUtils.isNotNull(params) && params instanceof BaseEntity){BaseEntity baseEntity = (BaseEntity) params;baseEntity.getParams().put(DATA_SCOPE, "");}}
}

为了便于理解参考,这里我就不去掉import的引入的类了,代码中可见,大概分为如下几个方法:

doBefore:这个方法是一个前置通知,即在目标方法执行前执行。它被用来清空数据范围过滤参数,然后调用 handleDataScope 方法处理数据范围过滤。

handleDataScope:该方法负责根据用户的角色和数据范围配置过滤数据。如果用户是超级管理员,则不进行数据过滤。否则,根据用户角色和权限配置调用 dataScopeFilter 方法进行数据范围过滤。

dataScopeFilter: 这是实际执行数据范围过滤的核心方法。根据用户的角色、权限和数据范围配置,动态构建 SQL 过滤条件,确保只返回用户有权限查看的数据。

clearDataScope:该方法用于在拼接权限 SQL 前先清空 params.dataScope 参数,以防止注入攻击。

这是四个方法的大概介绍,下面我们再整体梳理一下实现数据权限过滤的整体流程:

数据过滤的流程主要分为以下几个步骤:

  1. 切面拦截:

    • 使用 Spring AOP(Aspect-Oriented Programming)框架,通过切面(DataScopeAspect)拦截带有 @DataScope 注解的方法。
  2. 前置通知(doBefore 方法):

    • 在目标方法执行前执行前置通知,清空数据范围过滤参数(DATA_SCOPE)并调用 handleDataScope 方法。
  3. 处理数据范围(handleDataScope 方法):

    • 获取当前登录用户(LoginUser)以及用户的角色和权限信息。
    • 如果用户是超级管理员(isAdmin()),则不进行数据过滤。
    • 否则,根据用户角色的数据范围配置,调用 dataScopeFilter 方法生成数据范围的 SQL 过滤条件。
  4. 数据范围过滤(dataScopeFilter 方法):

    • 遍历用户的角色信息,根据每个角色的数据范围配置生成相应的 SQL 过滤条件。
    • 数据范围配置包括:
      • DATA_SCOPE_ALL:全部数据权限
      • DATA_SCOPE_CUSTOM:自定义数据权限
      • DATA_SCOPE_DEPT:部门数据权限
      • DATA_SCOPE_DEPT_AND_CHILD:部门及以下数据权限
      • DATA_SCOPE_SELF:仅本人数据权限
    • 根据用户的权限字符(permission)进行过滤。
    • 将生成的 SQL 过滤条件放入 params.dataScope 参数中,该参数会在 MyBatis 查询中使用。就是我们在mapper映射中见到的${params.dataScope}
  5. 清空数据范围参数(clearDataScope 方法):

    • 为防止注入攻击,再次在拼接权限 SQL 前先清空 params.dataScope 参数。之前我们提过一嘴,说是BaseEntity中的params可能造成SQL注入,这里就是进行了处理

总体来说,该流程保证了在用户执行查询操作时,根据用户的角色、权限和数据范围配置,动态生成相应的 SQL 过滤条件,以确保用户只能查询到其具有权限查看的数据。这种机制对于多租户系统或需要数据隔离的系统非常有用,可以有效地提高系统的安全性和数据隐私保护能力。

这样,我们就大概清楚了整个数据权限过滤的流程,但是我的基础不扎实,我对于JoinPoint一直耿耿于怀,不知道为什么这个对象里面会有接口的入参,我想还是有必要解释一下:

JoinPoint 是 Spring AOP 中的一个核心概念,它代表正在被拦截的连接点(Join Point)。在 Spring AOP 中,连接点是应用程序中可以被拦截的点,通常是方法的执行。JoinPoint 提供了许多有关连接点的信息,如方法名、目标对象、方法参数等。

在你提供的代码中,JoinPoint 作为参数出现在 doBefore 方法中,这个方法使用 @Before 注解标记,表示在目标方法执行之前执行。具体来说,doBefore 方法会在带有 @DataScope 注解的方法执行之前被调用。

在 AOP 中,切面(Aspect)是一种模块化的方式,用于横切关注点(cross-cutting concerns),例如日志、事务管理、权限控制等。JoinPoint 提供了切面方法中访问连接点信息的途径,使得你可以在目标方法执行前或执行后执行一些逻辑。

总之,JoinPoint 是 Spring AOP 中的一个对象,它允许你获取连接点的信息,从而在切面中执行相应的逻辑。

至此,我们就大概完成了代码分析,至于更加深层次的问题,我还需要不断学习补充能量,我们后会有期!希望这次阅读让您有所收获,如果否然,我们下次一定,抱歉!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/299796.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

反转链表算法及优化(leetcode第206题)

题目描述&#xff1a; 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1] 示例 2&#xff1a; 输入&#xff1a;head [1,2] 输出&#xff1a;[2,1] 示例…

【Linux系统编程】【Google面试题改编】线程之间的同步与协调 Linux文件操作

编写程序&#xff0c;有四个线程1、2、3、4 线程1的功能就是输1,线程2的功能就是输出2,以此类推……现在有四个文件ABCD初始都为空 现要让四个文件呈如下格式&#xff1a; A: 1 22 333 4444 1 22 333 4444… B: 22 333 4444 1 22 333 4444 1… C: 333 4444 1 22 333 4444 1 2…

分类预测 | Matlab实现SCSO-SVM基于沙猫群优化算法优化支持向量机的多变量分类预测【23年新算法】

分类预测 | Matlab实现SCSO-SVM基于沙猫群优化算法优化支持向量机的多变量分类预测【23年新算法】 目录 分类预测 | Matlab实现SCSO-SVM基于沙猫群优化算法优化支持向量机的多变量分类预测【23年新算法】分类效果基本描述程序设计参考资料 分类效果 基本描述 1.Matlab实现SCSO-…

【C++】STL 容器 - list 双向链表容器 ① ( 容器特点 | 容器操作时间复杂度 | 构造函数 )

文章目录 一、 list 双向链表容器简介1、容器特点2、容器操作时间复杂度3、遍历访问5、头文件 二、 list 双向链表容器 构造函数1、默认无参构造函数2、创建包含 n 个相同元素的 list 双向链表3、使用初始化列表构造 list 双向链表4、使用另外一个 list 容器 构造 list 双向链表…

Ascon加解密算法分析

参数定义 加密架构图 整个过程是在处理320bits的数据&#xff0c;所以在最开始需要对原始的数据进行一个初始化&#xff0c;获得320bits的数据块&#xff0c; 图里看到的pa和pb都是在做置换&#xff08;对320bits的数据进行一个置换&#xff09; 置换&#xff08;Permutation&…

智能优化算法应用:基于人工蜂鸟算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于人工蜂鸟算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于人工蜂鸟算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.人工蜂鸟算法4.实验参数设定5.算法结果6.…

C++11特性:线程同步之条件变量

条件变量是C11提供的另外一种用于等待的同步机制&#xff0c;它能阻塞一个或多个线程&#xff0c;直到收到另外一个线程发出的通知或者超时时&#xff0c;才会唤醒当前阻塞的线程。条件变量需要和互斥量配合起来使用&#xff0c;C11提供了两种条件变量&#xff1a; 1. conditi…

[Linux] MySQL数据库之事务

一、事务的概念 事务就是一组数据库操作序列&#xff08;包含一个或者多个 SQL 操作命令&#xff09;&#xff0c;事务会把所有 操作看作是一个不可分割的整体向数据库系统提交或撤消操作&#xff0c;所有操作要么都执行&#xff0c;要么都不执行。 事务是一种机制、一个操作序…

每日一题——LeetCode160.相交链表

个人主页&#xff1a;白日依山璟 专栏&#xff1a;Java|数据结构与算法|每日一题 文章目录 1. 题目描述示例1&#xff1a;示例2&#xff1a;提示&#xff1a; 2. 思路3. 代码 1. 题目描述 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的…

[pyqt5]QSpinBox相关函数

1.QSpinBox简介 QSpinBox是计数器控件&#xff0c;允许用户输入整数&#xff0c;或者通过上下按键递增或者递减&#xff0c;默认调整范围是0-99&#xff0c;每次变化步数1&#xff0c;用户可以自行修改范围和步数&#xff1b; QSpinBox常用方法如下&#xff1a; QSpinBox信号…

【LeetCode刷题笔记】动态规划(二)

647. 回文子串 解题思路: 1. 暴力穷举 , i 遍历 [0, N) , j 遍历 [i+1, N] ,判断每一个子串 s[i, j) 是否是回文串,判断是否是回文串可以采用 对撞指针 的方法。如果是回文串就计数 +1

入门级:用devEco Studio创建一个鸿蒙APP

文章概叙 本文主要讲的是如何在鸿蒙的开发工具devEco Studio新建一个项目&#xff0c;全文很水&#xff0c;只适合新手!! 开始贴图 假设当前你已经下载好了devEco Studio,但是还没正式开始安装&#xff0c;此时你点击安装包&#xff0c;你会发下如下页面&#xff0c;只需要点…