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

news2024/9/23 13:28:05

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

这篇文章,我也是作为一个优秀开源框架的学习者,在这里摸索这套框架是如何实现权限过滤的,这个封装对于入行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 u
		left join sys_dept d on u.dept_id = d.dept_id
		where 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;

    /** 搜索值 */
    @JsonIgnore
    private 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.coloradmin.cn/o/1335429.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Word-表格法对齐公式(手把手教学,公式格式从此不再愁)

新建word文件 1&#xff09;鼠标点击【插入】—>【表格】,选择31列的表格 2&#xff09;鼠标置于中间表格&#xff0c;快捷键输入Alt&#xff0c;进入公式编辑器中&#xff0c;输入任意字母&#xff0c;如&#xff1a;A&#xff0c;点击居中即可。 3&#xff09;第三列表…

【Kubernetes】控制器Statefulset

Statefulset控制器 一、概念二、Statefulset资源清单文件编写技巧2.1、查看定义Statefulset资源需要的字段2.2、查看statefulset.spec字段如何定义2.3、查看statefulset的spec.template字段如何定义 三、Statefulset使用案例&#xff1a;部署web站点3.1、编写一个Statefulset资…

地图服务器GeoServer的安装与配置

文章目录 1.安装配置Java2.安装配置Tomcat3 安装配置GeoServer GeoServer提供了多种安装配置方式&#xff0c;但是本质上GeoServer是一个基于Java Web的项目&#xff0c;因此我们理论上只需要安装Java&#xff0c;并且将其放置在一个Web服务器&#xff08;例如Apache Tomcat&am…

Redis缓存穿透、缓存击穿、缓存雪崩介绍

一、Redis的缓存穿透 1.什么是缓存穿透&#xff1f; 缓存穿透是指&#xff1a;客户端请求的数据在缓存中和数据库中都不存在&#xff0c;这时缓存就永远不会生效&#xff0c;这些请求都打到数据库从而导致数据库压力过大。 2.出现缓存穿透的解决方案&#xff0c;以下是常用的两…

解决远程操作报报错函数不支持问题

1、虚拟机远程里面勾了仅允许使用网络.....取消 2、检查虚拟机防火墙是不是没有添加入栈规则 一个ICMPv6一个ICMPv6要开放

SpringMVC:整合 SSM 下篇

文章目录 SpringMVC - 05整合 SSM 下篇一、设计页面1. 首页&#xff1a;index.jsp2. 展示书页面&#xff1a;showBooks.jsp3. 增加书页面&#xff1a;addBook.jsp4. 修改书页面&#xff1a;updateBook.jsp5. 总结 二、控制层1. 查询全部书2. 增加书3. 修改书4. 删除书5. 搜索书…

linux系统和网络(四):网络

本文主要探讨linux网络相关知识,详细介绍看本博客其他博文。 网络基础(参考本博客其他文章&#xff1a;基础网络知识&#xff0c;socket网络编程&#xff0c;基于socket的聊天室和简易ftp) 路由器是局域网和外部网络通信出入口 DNS实现域名和IP地址之间转换 …

Swagger快速入门

1、Swagger快速入门 1.1 swagger介绍 官网&#xff1a;https://swagger.io/ Swagger 是一个规范和完整的Web API框架&#xff0c;用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。 功能主要包含以下几点: A. 使得前后端分离开发更加方便&#xff0c;有利于团队协作…

软件测试面试八股文——基础篇

5&#xff09;错误推测法&#xff1a;是基于经验和直觉推测程序中所有可能存在的各种错误&#xff0c;从而有针对性的设计测试用例的方法 6&#xff09;正交实验法 7&#xff09;判定表法 8&#xff09;测试大纲法 3、提交缺陷的八大要素 1&#xff09;缺陷编号&#xff1a…

选择排序之C++实现

描述 选择排序&#xff08;Selection Sort&#xff09;是一种简单直观的排序算法。它的基本思想是&#xff1a;每一轮从待排序的数据中选择最小&#xff08;或最大&#xff09;的一个元素&#xff0c;然后与待排序数据的第一个元素交换位置。对剩余未排序的数据重复这个过程&a…

CentOS进入单用户模式

一、重启 二、出现内核选项 按“e” 三、编辑这一行 输入 rw init/sysroot/bin/sh 四、进入单用户模式 ctrlx 进入 五、切换目录 chroot /sysroot 六、然后你就操作你的系统了。 修改密码等等

OCP NVME SSD规范解读-1

OCP&#xff08;Open Compute Project&#xff09;是一个由Facebook于2011年发起的开源项目。其目标是重新设计和优化数据中心的硬件&#xff0c;包括服务器、存储、网络设备等&#xff0c;以提高效率&#xff0c;降低运营成本&#xff0c;并推动技术的创新和标准化。 在OCP中&…

count distinct在spark中的运行机制

文章目录 预备 数据和执行语句Expand第一次HashAggregateShuffle and Second HashAggregate最后结果性能原文 预备 数据和执行语句 SELECT COUNT(*), SUM(items), COUNT(DISTINCT product), COUNT(DISTINCT category) FROM orders;假设源数据分布在两个1核的结点上&#xff0…

Java Web基础详解

回顾 之前的两篇的文章已经大概的带我们了解了tomcat的一些基本的操作&#xff0c;比如从零搭建我们自己的调试环境以及官方文档构建的方式&#xff0c;接下来的话&#xff0c;我将带大家来了解一下tomcat的一些基础知识&#xff0c;这些基础知识将以问题的方式抛出&#xff0…

IDEA Maven Helper插件 解决jar冲突

Jar包冲突报错 程序抛出java.lang.ClassNotFoundException异常&#xff1b; 程序抛出java.lang.NoSuchMethodError异常&#xff1b; 程序抛出java.lang.NoClassDefFoundError异常&#xff1b; 程序抛出java.lang.LinkageError异常等&#xff1b;Maven Jar包管理机制 在Maven项…

《Halcon 100项目-2》Halcon查找零件个数

Halcon查找零件个数 read_image (Image20231225201927, D:/image/bilibili/photo/屏幕截图 2023-12-25 201927.png) rgb1_to_gray (Image20231225201927, GrayImage)threshold (GrayImage, Region, 0, 128) draw_rectangle1 (200000, Row1, Column1, Row2, Column2) gen_recta…

年龄相关的微环境变化突显了PDGF-C在ER+乳腺癌转移复发中的作用

今天给同学们分享一篇实验文章“Age-associated microenvironmental changes highlight the role of PDGF-C in ER breast cancer metastatic relapse”&#xff0c;这篇文章发表在Nat Cancer期刊上&#xff0c;影响因子为22.7。 结果解读&#xff1a; ER乳腺癌转移复发的同种…

三相电机转差率为负值的情形

1.电机开始发电的特征 注意&#xff0c;电机因为有输入频率对原始旋转磁场的影响&#xff0c;在正常工作时&#xff0c;应该处于稳态&#xff0c;因为旋转磁场决定了这个系统的运转方向和运转的大致频率区间。它会处于力矩平衡态。但是&#xff0c;如果&#xff0c;此时电机处…

帕累托森林CEO李朝政博士受邀「OSS-Compass」开源年会畅谈:开源框架下的奇异竞争规则

导语 “怎么在别人知道你的代码和战略时&#xff0c;你仍然拥有壁垒&#xff1f;”这是开源框架下&#xff0c;商业必须思考的问题。 软件驱动世界运转。但当基础层代码失去了有深度活力的“动态延伸”潜力&#xff0c;便会遭致市场“零定价”的宿命。动态延伸的效率&#xf…

MES系统是什么?MES系统的功能有哪些?

在现代制造业的快速发展中&#xff0c;所有规模的企业都面临着类似的挑战&#xff1a;如何提高生产效率、确保产品质量、减少浪费、降低成本&#xff0c;同时迅速响应市场变化。而在这个过程中&#xff0c;传统企业管理往往有以下几个典型痛点&#xff1a; 纸质文件堆叠如山&a…