若依中是如何添加数据权限的

若依系统实现了以下数据权限:

  1. 全部数据权限
  2. 自定义数据权限
  3. 部门数据权限
  4. 部门及以下数据权限
  5. 仅本人数据权限

问题起源

前一段给公司的后台管理系统做权限管理,领导提出三步走:

  1. 首先做页面的权限控制,即用户不应该看到他无权操作的内容;

这一部分我们使用后台动态生成路由,或者由前端Vue-Router做权限控制,可以做到页面的权限控制;页面按钮的权限控制可以通过Vue的指令,来动态控制其是否显示;

  1. 第二步是做接口的权限控制,即用户无权访问的接口,就访问不到;

这一步,我们借助于开源框架Shiro来实现,通过Shiro可以做到接口的权限控制;样例如下:

1
2
3
4
@RequiresPermissions("user:list")
public void list() {
// ...
}
  1. 第三步是做数据的权限控制,即用户无权访问的数据,就访问不到;

数据的权限控制较为复杂,到底是根据用户的ID来做复杂的限制呢,还是根据用户的角色做限制呢?

在学习的开源框架若依中,它又是怎么做的呢?我们来学习研究一下。

若依中的数据权限控制

在若依的部分接口中,可以看到如下注解:

1
2
3
4
5
6
 @DataScope(deptAlias = "d", userAlias = "u")
public List<SysUser> selectUserList(SysUser user)
{
return userMapper.selectUserList(user);
}

该注解DataScope是若依中自己定义的一个注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.ruoyi.common.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 数据权限过滤注解
*
* @author ruoyi
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope
{
/**
* 部门表的别名
*/
public String deptAlias() default "";

/**
* 用户表的别名
*/
public String userAlias() default "";
}

注解中的两个参数分别表示部门表的别名与用户表的别名。

在前几天学习注解的过程中,我们知道,注解的定义并不能解释注解的执行逻辑,而更重要的是注解的解释执行器。

在项目中搜索一下关键字符:DataScope。在模块ruoyi-framework中,找到了类DataScopeAspect

先来看看该类中的几个常量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@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";
}

根据注释,我们猜测,其实现了按照以下粒度控制数据访问权限:

  1. 全部数据权限
  2. 自定义数据权限
  3. 部门数据权限
  4. 部门及以下数据权限
  5. 仅本人数据权限

执行代码分析

查看DataScopeAspect类的源码,我们可以看到其中的关键方法是:dataScopeFilter,调用逻辑是:

doBefore -> handleDataScope -> dataScopeFilter

我们打个断点看看:

20220521105741

成功拦截!

进一步,我们逐步执行,终于发现其对SQL的修改行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* 数据范围过滤
*
* @param joinPoint 切点
* @param user 用户
* @param userAlias 别名
*/
public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias)
{
StringBuilder sqlString = new StringBuilder();

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(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(" OR 1=0 ");
}
}
}

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) + ")");
}
}
}

已我们登陆的账号ry为例:

其角色权限为2,即“自定义数据权限”:

执行过程中,首先:

1
sqlString = " OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) "

然后多个角色权限叠加OR,得到:

1
" OR ..."

最后修改参数JoinPoint中的params.dataScope,其值为:

1
" AND (权限控制语句)" 

而实际执行的SQL中有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<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.password, 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>

注意看最后的一句:

1
2
3
  原mysql
<!-- 数据范围过滤 -->
${params.dataScope}

拼接后,完整sql语句为:

1
2
原sql
AND (权限控制语句)

如此,便实现了数据权限控制。