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

2023-12-26 04:30

本文主要是介绍摸索若依框架是如何实现权限过滤的,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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

这篇文章,我也是作为一个优秀开源框架的学习者,在这里摸索这套框架是如何实现权限过滤的,这个封装对于入行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.chinasem.cn/article/538104

相关文章

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略 1. 特权模式限制2. 宿主机资源隔离3. 用户和组管理4. 权限提升控制5. SELinux配置 💖The Begin💖点点关注,收藏不迷路💖 Kubernetes的PodSecurityPolicy(PSP)是一个关键的安全特性,它在Pod创建之前实施安全策略,确保P

pandas数据过滤

Pandas 数据过滤方法 Pandas 提供了多种方法来过滤数据,可以根据不同的条件进行筛选。以下是一些常见的 Pandas 数据过滤方法,结合实例进行讲解,希望能帮你快速理解。 1. 基于条件筛选行 可以使用布尔索引来根据条件过滤行。 import pandas as pd# 创建示例数据data = {'Name': ['Alice', 'Bob', 'Charlie', 'Dav