Ruo-Yi前后端分离的数据过滤
若依官网的介绍:http://doc.ruoyi.vip/ruoyi/document/htsc.html#%E6%95%B0%E6%8D%AE%E6%9D%83%E9%99%90
1、数据过滤的实现
其实是在mybatis 的 sql 上添加了${params.dataScope}
这个东西,实现了数据过滤。
<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') >= 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') <= 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>
为什么加这个就可以实现呢?我们根据/system/user/list
这个请求路径来分析?
@PreAuthorize("@ss.hasPermi('system:user:list')")
@GetMapping("/list")
public TableDataInfo list(SysUser user)
{
// 设置 分页 参数
startPage();
// 根据分页参数查询数据 表:sys_user 和 sys_dept
List<SysUser> list = userService.selectUserList(user);
// 封装前台需要的数据
return getDataTable(list);
}
然后我们进入selectUserList
方法
@Override
@DataScope(deptAlias = "d", userAlias = "u")
public List<SysUser> selectUserList(SysUser user)
{
return userMapper.selectUserList(user);
}
在这里我们发现了关键,就是==@DataScope==注解,其实若依框架的数据过滤就是通过这个注解实现的。这个是一个AOP知识点的利用,我们需要找到标注了@Aspect
的注解,我们找到了类DataScopeAspect
。之后我们要进入 selectUserList()
方法之前,我们先要进入DataScopeAspect
类。
方法的调用流程:
进入切面类 DataScopeAspect ,首先进入
@Before("@annotation(controllerDataScope)")
public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable
{
clearDataScope(point); // 拼接权限sql前先清空params.dataScope参数防止注入
handleDataScope(point, controllerDataScope);
}
然后:
protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope)
{
// 获取当前的用户
LoginUser loginUser = SecurityUtils.getLoginUser(); // 获取当前登陆的用户
if (StringUtils.isNotNull(loginUser))
{ // 如果 loginUser 不为空的话
SysUser currentUser = loginUser.getUser();
// 如果是超级管理员,则不过滤数据
if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin())
{// 下面就要过滤数据了
String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(), PermissionContextHolder.getContext());
// 这里其实就是 想给 每一个实体类都会继承的 BaseEntity 里面的 Map<String, Object> params; 属性添加一些过滤条件
// 之后再 mapper 生成sql的时候就可以用来 过滤数据了
dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
controllerDataScope.userAlias(), 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>(); // 存储 数据权限 1,2,3,4,5
// 获取当前用户 的全部角色信息
for (SysRole role : user.getRoles())
{
String dataScope = role.getDataScope(); // 获取 当前角色的数据权限
// String DATA_SCOPE_CUSTOM = "2"; => 自定数据权限
// 如果当前的 数据权限 不是 自定数据权限 并且 conditions 里面不包含当前的 数据权限
if (!DATA_SCOPE_CUSTOM.equals(dataScope) && conditions.contains(dataScope))
{
continue;
}
// 如果 ①permission 不为空 并且 ② 角色的权限信息不为空 并且 ③角色的权限不包含传递过来的角色权限
if (StringUtils.isNotEmpty(permission) && StringUtils.isNotEmpty(role.getPermissions())
&& !StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission)))
{
continue;
}
//===================================== 从这里之后,sqlString 前面被赋值后,后面要是再次赋值会覆盖前面的 ====================================
// String DATA_SCOPE_ALL = "1"; 全部数据权限
// 如果当前的 数据权限是 全部数据权限
if (DATA_SCOPE_ALL.equals(dataScope))
{
// 如果遍历到 一个 角色的数据权限为1的时候,就是 全部数据权限 就不需要过滤了,直接返回
sqlString = new StringBuilder();
break;
}
// String DATA_SCOPE_CUSTOM = "2"; => 自定数据权限
// 如果当前的 数据权限是 自定数据权限
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()));
}
// String DATA_SCOPE_DEPT = "3"; => 部门数据权限
// 如果当前的 数据权限是 部门数据权限
else if (DATA_SCOPE_DEPT.equals(dataScope))
{
sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
}
// String DATA_SCOPE_DEPT_AND_CHILD = "4"; => 部门及以下数据权限
// 如果当前的 数据权限是 部门及以下数据权限
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()));
}
// String DATA_SCOPE_SELF = "5"; => 仅本人数据权限
// 如果当前的 数据权限是 仅本人数据权限
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);
}
//====================上面是为了 设置 需要拼接的sql,下面是为将 sql 设置到实体类里面 =======================
// 如果 sqlString不为 空的话
if (StringUtils.isNotBlank(sqlString.toString()))
{
Object params = joinPoint.getArgs()[0];
if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
{
BaseEntity baseEntity = (BaseEntity) params;
// DATA_SCOPE = "dataScope";
// 因为每一个实体类都继承了 BaseEntity 类,所以,每一个类也就有里面的每一个属性
// 这里进行的操作就是给,实体类的 Map<String, Object> params; 字段 put("dataScope","xxxx")
baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
}
}
}
然后就进入:
@Override
@DataScope(deptAlias = "d", userAlias = "u")
public List<SysUser> selectUserList(SysUser user)
{
return userMapper.selectUserList(user);
}
执行sql去了,这个时候 user 对象因为经过 DataScopeAspect
类的一大堆骚操作,因为每一个实体类都继承了 BaseEntity 类,所以,每一个类也就有里面的每一个属性,经过上面的方法 给 params属性增加了key :"dataScope"值:“xxxx”
然后再 mybatis 的sql里面就可以取到${params.dataScope}
,这个东西取到的就是sqlString
字符串
<select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult">
....
<!-- 数据范围过滤 -->
${params.dataScope}
</select>
总结:
1、如果我们再若依项目里面想用数据过滤其实就只需要再 service 层加上一个 @DataScope
注解即可
2、若依框架能实现数据过滤的主要原因就是利用了AOP切面,再进入 service 方法之前先进入切面类里面的方法,执行完毕后再进入 service 层的方法。
2、数据过滤测试每一种情况
下面我们根据系统管理下的用户管理页面作为分析,查看每一种数据权限。
调用的接口:
http://localhost/dev-api/system/user/list?pageNum=1&pageSize=10
测试的数据
INSERT INTO `sys_user` VALUES (1, 103, 'admin', '若依', '00', 'ry@163.com', '15888888888', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', '2022-10-19 14:22:42', 'admin', '2022-09-30 22:02:23', '', '2022-10-19 14:22:42', '管理员');
INSERT INTO `sys_user` VALUES (2, 105, 'ry', '若依', '00', 'ry@qq.com', '15666666666', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', '2022-09-30 22:02:23', 'admin', '2022-09-30 22:02:23', 'admin', '2022-10-17 11:51:08', '测试员');
INSERT INTO `sys_user` VALUES (100, NULL, '111', '1', '00', '', '', '0', '', '$2a$10$t0cnTqGNcOZlx99Hj3FIk.C3.4tFvtperOpp8nmhlhOl/vax.DjZq', '0', '0', '', NULL, 'admin', '2022-10-13 14:09:55', 'admin', '2022-10-17 11:51:10', NULL);
INSERT INTO `sys_user` VALUES (101, 101, 'zs', 'zs', '00', '', '', '0', '', '$2a$10$MI9C2H3sw6orfGUcGOQoZushCERPPYmF0RCJFapI8ShBrU27bH8s6', '0', '0', '127.0.0.1', '2022-10-19 14:16:16', 'admin', '2022-10-13 17:48:39', 'admin', '2022-10-19 14:16:16', NULL);
INSERT INTO `sys_user` VALUES (102, 101, 'sz', '深圳总公司', '00', '', '', '0', '', '$2a$10$Z1qGn6CKHZ9UgMpCILsKoO3Bgsc.oOOswqhChqrFpgAklFrDMzy6C', '0', '0', '', NULL, 'admin', '2022-10-19 11:45:10', '', NULL, NULL);
2.1、① 仅自己权限
现象
登陆zs用户查询,zs用户的数据权限
只有仅自己
。查询结果:
分析
相当于查询用户id只为当前登陆用户
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));
}
查询的SQL:
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'
AND ( u.user_id = 101 )
LIMIT 10;
2.2、② 本部门
现象:
登陆zs用户查询,zs用户的数据权限
只有本部门
。查询结果:
分析
根据当前用户的部门
信息过滤数据
sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
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'
AND ( d.dept_id = 101 )
LIMIT 10;
其实是给原本的SQL拼接了:
AND ( d.dept_id = 101 )
2.3、③ 本部门及以下数据权限
现象
登陆zs用户查询,zs用户的数据权限
只有本部门及以下数据权限
。查询结果:
分析
根据当前用户本部门及以下数据权限
信息过滤数据
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()));
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'
AND (d.dept_id IN (SELECT dept_id FROM sys_dept WHERE dept_id = 101 OR find_in_set(101, ancestors) ))
LIMIT 10;
其实是给原本的 SQL 拼接了:
AND (d.dept_id IN (SELECT dept_id FROM sys_dept WHERE dept_id = 101 OR find_in_set(101, ancestors) ))
2.4、④ 全部数据权限
现象
登陆zs用户查询,zs用户的数据权限
只有全部数据权限
。查询结果:
全部都可以查询出来
分析
相当于给原本的 sql 后面什么 都不拼接
sqlString = new StringBuilder();
break;
查询数据的sql语句:
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'
LIMIT 10;
相当于什么数据没有过滤。
2.5、⑤ 自定数据权限
现象
设置自定义数据权限
只能看到研发部门。
# 题外话:后面会详细讲解
# 1
在按下确定的这一瞬间其实发起了请求`http://localhost/dev-api/system/role/dataScope`
这个请求往数据库表`sys_role_dept`插入数据,这个表就是 自定义数据权限才会用到的表
# 2
这里 还存在一个 父子联动的问题,按照我这里显示的问题,这里我们选择父子联动
- 如果选择父子联动: 用户属于若依科技、深圳总公司、研发部门,这三个任意一个都可以查询出来
- 如果不选择父子联动:只能查询用户属于 深圳总公司下研发部门的用户
登陆zs用户查询,zs用户的数据权限
只有自定数据权限
。查询结果
分析
根据当前用户本部门及以下数据权限
信息过滤数据
相当于根据角色id
在ssys_role_dept
表里面找到部门id,然后根据部门id进行过滤数据
sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
role.getRoleId()));
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'
AND ( d.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = 104 ) )
LIMIT 10;
3、父子联动问题
前面我们不是有说到一个 父子联动
的按钮吗,我们现在就来详细来看看,这里为了更加能说明问题,我在数据库有添加了一个用户。她是属于长沙分公司的研发部门员工。
INSERT INTO `sys_user` VALUES (103, 205, 'csyf', '长沙-研发1', '00', '', '', '0', '', '$2a$10$VdGvUO.ZHGgAFCGBXMNBA.sjOtnYoODeqYYkrJhuqC4etrh/m.sXK', '0', '0', '', NULL, 'admin', '2022-10-19 14:08:46', '', NULL, NULL);
3.1、① 不勾选父子联动
我们先来看第①种情况,父子不联动,我们只选择 深圳分公司下面的 研发部门
点击确定后,我们发现请求了地址:http://localhost/dev-api/system/role/dataScope
保存到数据库表sys_role_dept
里面的也只有一条数据
admin用户的页面:
我们现在再使用zs用户登陆系统。根据我们上面的分析,过滤的时候过滤的时候是查询当前角色下的部门id,根据这些部门id进行过滤的。所以 我们也就只能看到 深圳分公司下面研发部门的用户,即只能看到zs用户。
3.2、② 勾选父子联动
下面我们再看一下勾选父子联动,选择深圳分公司下面的研发部门会发生什么。
点击确定后,我们发现请求了地址:http://localhost/dev-api/system/role/dataScope
再看看数据库:
admin用户的页面:
我们现在再使用zs用户登陆系统。根据我们上面的分析,过滤的时候过滤的时候是查询当前角色下的部门id,根据这些部门id进行过滤的。所以 我们也就只能看到 深圳分公司下面研发部门的用户,即我们可以看到 admin、zs、sz这三个用户。