一、概述

本文主要整理了若依框架中数据权限的功能实现,页面功能位于【系统管理 > 角色管理 > 数据权限】。
部门数据权限
自定义数据权限

二、说明与举例

数据权限的使用在后台管理系统十分常见,一般情况而言,数据权限是 配合角色进行使用 ,常用的数据权限划分包括部门以及用户个人。

数据权限和角色权限配合起来控制展示给用户的数据。如果分不清楚两个权限的区别,可以这样理解:角色权限通常是控制菜单和按钮,即用户能够看到什么功能,做什么操作;数据权限则通常是根据用户本身的职务过滤后台返回的数据。

举个例子,某个后台管理系统划分了三个模块以及每个模块分配了对应的角色(表1):

系统模块角色A角色B角色C
人事模块人事专员人事经理领导
业务模块销售专员销售经理领导
财务模块财务专员财务总监领导

由上表,三个系统模块对应着三个部门,每个部门中角色B是部门负责人,角色C为统一领导人,因而此处赋予的角色数据权限如下(表2):

系统模块角色A角色B角色C
人事模块本部门数据权限本部门数据权限全部数据权限
业务模块本人数据权限本部门数据权限全部数据权限
财务模块本部门数据权限本部门数据权限全部数据权限

注 1:表 2 的数据权限不是固定的,可以根据真实业务进行灵活分配。为了理解简单起见,假设每个模块都只有一个列表数据,即每个部门都只有一个菜单和数据表。

注 2:这里只是简单举例,没有实际代码和表进行展示,后面的说明是从原理上进行说明,感兴趣的朋友可以根据上面的业务自行拓展练习。

三、逻辑实现过程

1、数据权限的使用

数据权限可以针对单表以及多表关联等不同维度进行,所以先从简单的单表进行说明。

1.1、单表数据权限

若依使用了自定义注解 @DataScope 实现数据权限的功能。需要在对应的接口加上该注解,并在加入 Mybatis-plus 中 .apply() 方法对SQL进行注入即可实现数据的过滤。
数据权限使用
这里需要特别强调的是,该框架对于数据权限功能进行了优化,因而对于官方文档中使用别名的情况是在多表中进行使用,单表只需要直接加上注解即可。

当然,如果想要在xml文件里面做单表的数据权限过滤也可以,参考 system 模块里面的 selectDeptList 查询部门管理数据接口。
单表过滤接口
xml单表

1.2、多表数据权限

单表数据权限的实现一般是在相关数据库表中冗余了相关权限字段,例如部门id dept_id。而某些场景下需要多表关联进行数据权限的过滤。由于 Mybatis-plus 条件构造器目前不支持多表关联查询,因此需要在xml中进行数据权限的过滤。
多表查询
接口中的别名要与xml文件中定义的别名一致。
xml多表

2、数据权限的实现

无论是单表还是多表数据权限,其核心部分都是SQL的组装,因此只需要了解SQL的组装过程,那么对于数据权限的实现应该也能了然于心了。
注解
对于数据权限的核心处理类是 DataScopeAspect
数据权限类型
织入点
注 3:这里涉及到AOP相关的知识,如果不熟悉AOP的话建议先找来看下,篇幅所限,这里不再具体展开。

@Pointcut("@annotation(com.ruoyi.common.annotation.DataScope)") 表示是通过注解实现切面。
@Before("dataScopePointCut()") 代表在织入点方法执行前进行。

处理数据权限之前先清空相关参数,putDataScope()方法后面会细讲。
在这里插入图片描述
若依数据权限的参数是 BaseEntity 中的 private Map<String, Object> params = new HashMap<>();,在上面的xml中也有${params.dataScope}的使用。

Map 中的 key 是 dataScope ,value是拼接好的 SQL 语句
key
参数清空后,处理数据权限。
在这里插入图片描述
前面是获取注解的内容,判断当前用户是否需要过滤,一切准备就绪之后,终于到核心方法 dataScopeFilter()


	... 省略部分代码 ...
	
	/**
	 * 数据范围过滤
	 *
	 * @param joinPoint 切点
	 * @param user      用户
	 * @param userAlias 别名
	 */
	public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, boolean isUser) {
		StringBuilder sqlString = new StringBuilder();

		// 将 "." 提取出,不写别名为单表查询,写别名为多表查询
		deptAlias = StrUtil.isNotBlank(deptAlias) ? deptAlias + "." : "";
		userAlias = StrUtil.isNotBlank(userAlias) ? userAlias + "." : "";

		for (SysRole role : user.getRoles()) {
			String dataScope = role.getDataScope();
			// 全部数据权限
			if (DATA_SCOPE_ALL.equals(dataScope)) {
				sqlString = new StringBuilder();
				break;
			} 
			// 自定数据权限
			else if (DATA_SCOPE_CUSTOM.equals(dataScope)) {
				sqlString.append(StrUtil.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(StrUtil.format(" OR {}dept_id = {} ",
					deptAlias, user.getDeptId()));
			} 
			// 部门及以下数据权限
			else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) {
				sqlString.append(StrUtil.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 (isUser) {
					sqlString.append(StrUtil.format(" OR {}user_id = {} ",
						userAlias, user.getUserId()));
				} else {
					// 数据权限为仅本人且没有userAlias别名不查询任何数据
					sqlString.append(" OR 1=0 ");
				}
			}
		}

		if (StrUtil.isNotBlank(sqlString.toString())) {
			putDataScope(joinPoint, sqlString.substring(4));
		}
	}
	
	... 省略部分代码 ...

	private static void putDataScope(JoinPoint joinPoint, String sql) {
		// 获得切入点的参数
		Object params = joinPoint.getArgs()[0];
		if (Validator.isNotNull(params)) {
			// 判断参数是否属于 BaseEntity 或者其子类
			if (params instanceof BaseEntity) {
				BaseEntity baseEntity = (BaseEntity) params;
				// 将 SQL 放入 params 字段中
				baseEntity.getParams().put(DATA_SCOPE, sql);
			} 
			// 参数没有继承 BaseEntity
			else {
				try {
					// 反射获取参数中的 params 字段 getter 方法
					Method getParams = params.getClass().getDeclaredMethod("getParams", null);
					Map<String, Object> invoke = (Map<String, Object>) getParams.invoke(params, null);
					// 将 SQL 放入 params 字段中
					invoke.put(DATA_SCOPE, sql);
				} catch (Exception e) {
					// 方法未找到 不处理
				}
			}
		}
	}

这里代码看起来很长,所以断点会比较容易明白每个判断的内容是什么。

  • 全部数据权限
    在这里插入图片描述
    该权限不做数据过滤,因此没有进行SQL拼接。页面返回所有的数据。
    在这里插入图片描述

  • 自定数据权限
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述在这里插入图片描述
    自定义数据权限实际上是自定义所能看到的部门,根据角色关联的部门过滤相关数据。
    在这里插入图片描述

  • 部门数据权限
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  • 部门及以下数据权限

该权限与部门数据权限的区别在于用了 find_in_set 函数筛选部门。
在这里插入图片描述
因为没有下属部门所以数据是一样的,但是SQL是不同的。
在这里插入图片描述

  • 仅本人数据权限

本人数据权限需要在注解加上isUser = true
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

至此,5种数据权限都已经解析完了。

四、可能遇到的问题

1、修改数据权限没有生效?

若依框架中修改数据权限不会清空用户数据缓存,因此需要重新登录新的数据权限才会生效。

2、为什么需要 sqlString.substring(4) ?

在这里插入图片描述

框架中多个数据权限之间是用关键字 OR 来进行拼接的,如果没有 SQL 就会报错,当然只有一个权限时可能看不出来这个问题,所以最后合成的 SQL 需要去掉第一个 OR。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐