Jeecg如何实现数据权限/隔离?用户上下文变量没有user

前言:

在使用如若依、Jeecg等后台管理系统进行二次开发的时候,我们总会涉及到数据隔离相关的内容,如每个非管理员用户应该都只能看到自己创建的数据,而不是所有的数据,本文将以jeecg为例像大家介绍如何每个用户之间的数据隔离的效果。

先从功能需求说起,当查询房屋列表的时候,每个房东(用户)应该只能看到自己的房屋(house)表数据,而房屋和房东的关系存放于房东(house_holder)表中,所以我们需要通过两个表的关联来实现数据隔离的查询。而jeecg中要如何实现呢?我们一起啦看看吧

基于Jeecg实现:

先放出官方文档中跟数据隔离有关的内容:doc.jeecg.com/2044046

  1. 进入菜单管理页面,找到想要进行数据隔离的菜单,点击添加一个下级并进行配置

image.png
选择按钮/权限并配置菜单路径(笔者能力有限暂时无法弄清菜单路径和授权表示的作用在哪)
image.png
然后对添加的这个按钮/权限进行数据规则的配置,
由于我们需要要从另外一张表中去查出跟user_id有关的house_id所以要使用自定义SQL的方式

image.png

  1. 在角色管理中对用角色进行数据权限的授权
    给需求中的房东用户配置上数据权限

image.png

  1. 在后端对应的controller中的list方法中加上@PermissionData注解,该注解有一个pageComponent参数为数据隔离的菜单的前端组件路径,如果配置了只有该前端组件路径会被拦截,不配置的话就拦截全部请求。

大家可能注意到了,官方文档中给出了系统上下文变量有sys_user_code, 有sys_user_name,有sys_org_code但就是没有SQL表达式中的#{sys_user_id},而我们想要实现的效果只能给予user_id来实现,那么我们要怎么实现呢?

修改底层源码

笔者先给出如何修改,实现将当前登录用户的id存入数据权限的系统上下文变量中,来探讨其源码是如何进行修改的。

以下文件直接通过idea的search every where 的功能即可找到对应类

3.1. SysUserCacheInfo中添加一个String类型的sysUserId和其对应的setter/getter

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码...
private String sysUserId;

// 用idea生成setter/getter,如果懒的话直接用@Data然后删除其他的setter/getter也成
public String getSysUserId() {
return sysUserId;
}

public void setSysUserId(String sysUserId) {
this.sysUserId = sysUserId;
}
...

3.2. SysBaseApiImpl中的getCachUser为3.1的CacheInfo的新增的属性赋值

1
2
3
4
5
6
7
8
9
10
11
java复制代码public SysUserCacheInfo getCacheUser(String username) {
SysUserCacheInfo info = new SysUserCacheInfo();
info.setOneDepart(true);
LoginUser user = this.getUserByName(username);
if(user!=null) {
//加上下面一行代码
info.setSysUserId(user.getId());
info.setSysUserCode(user.getUsername());
...
}
...

3.3. DataBaseConstant中添加数据权限的系统上下文的key值

1
2
3
4
5
6
7
8
9
java复制代码...
public static final String SYS_USER_CODE_TABLE = "sys_user_code";
/**
* 添加下面这一行代码
*/
String SYS_USER_ID = "sys_user_id";

public static final String SYS_USER_NAME = "sysUserName";
...

3.4. 在JwtUtil中的getUserSystemData放将sys_user对应的value设置为当前用户的id

1
2
3
4
5
6
7
8
9
10
11
java复制代码...
if(key.equlas(...){

}else if (key.equals(DataBaseConstant.TENANT_ID) || key.toLowerCase().equals(DataBaseConstant.TENANT_ID_TABLE)){
...
}
// 加上这一个else if 的判断就完成啦
}else if (key.equals(DataBaseConstant.SYS_USER_ID)){
returnValue = sysUser.getId();
}
...

基于上面的4个步骤的小改动,我们就能够成功的为jeecg的数据权限添加上当前用户id值了。然后进行测试应该就能发现我们成功进行了基于user_id的多表关联的数据隔离啦。

底层深探

通过对@PermissionData注解的跳转,我们能看到其对应着一个名为PermissionDataAspect的切面

image.png

在切面中查询出了对应用户的数据权限模型(PermissionDataRuleModel),并且于当前用户的userInfo都放入了request请求中

image.png
数据权限模型的值如下,可以看到,其值就是我们刚刚在后台中配置的数据规则

image.png

而JeecgDataAuthorUtils.installUserInfo的代码如下:

image.png

其将userinfo存放在了request当中(setter),但该切面到此处就结束了。 我们再看看整个进入Controller的list方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码/**
* 分页列表查询
*
* @param house
* @param pageNo
* @param pageSize
* @param req
* @return
*/
@AutoLog("房产主表-分页列表查询")
@ApiOperation(value="房产主表-分页列表查询", notes="房产主表-分页列表查询")
@GetMapping("/list")
@PermissionData
public IPage<House> queryPageList(House house,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<House> queryWrapper = QueryGenerator.initQueryWrapper(house, req.getParameterMap());
Page<House> page = new Page<House>(pageNo, pageSize);
IPage<House> pageList = houseService.page(page, queryWrapper);
return pageList;
}

有一个QueryGenerator.initQueryWrapper其传入了Request的ParameterMap,这个parameterMap中包含了在切面是存入的userInfo和datarulemodel,所以数据的隔离会不会是在这里实现的呢? 我们在进去看看,

image.png

再进入这里installMplus方法:

image.png

看到这,我们应该就找到了jeecg中居于mybatis plus实现的数据权限/隔离的代码所在地啦,但这个类十分的冗余判断,难以阅读,所以作者给出几个该类中和数据权限有关的代码:

installMplus方法中通过我们之前在切面中传入的SysPermissionDataRuleModel开始解析我们自定义的SQL语句中的模板变量。

image.png
这个getSqlRuleValue方法就是模板替换的核心啦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public static String getSqlRuleValue(String sqlRule){
try {
//获取sql语句中的模板变量:实际就是通过正则匹配sqlRule中的${}字符
Set<String> varParams = getSqlRuleParams(sqlRule);
for(String var:varParams){
// 根据模板变量的key值来返回具有实际意义的value:通过JwtUtil的getSystemData方法来返回value
String tempValue = converRuleValue(var);
// 替换模板变量为真正的value
sqlRule = sqlRule.replace("#{"+var+"}",tempValue);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return sqlRule;
}

public static String converRuleValue(String ruleValue) {
String value = JwtUtil.getUserSystemData(ruleValue,null);
return value!= null ? value : ruleValue;
}

getSqlRuleValue方法的debug变量如下
image.png

JwtUtil中的getSystemData的代码如下:其实就是一大堆的if-else + contstant常量罢了

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
java复制代码/**
* 从当前用户中获取变量
* @param key
* @param user
* @return
*/
public static String getUserSystemData(String key,SysUserCacheInfo user) {
if(user==null) {
user = JeecgDataAutorUtils.loadUserInfo();
}
// 获取登录用户信息
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();

...一些正则匹配

//替换为系统登录用户帐号
if (key.equals(DataBaseConstant.SYS_USER_CODE)|| key.toLowerCase().equals(DataBaseConstant.SYS_USER_CODE_TABLE)) {

}
... 一些对于DataBaseContants中的KEY的else-if判断

}else if (key.equals(DataBaseConstant.SYS_USER_ID)){
returnValue = sysUser.getId();
}

return returnValue;
}

总结

数据隔离实际上就是对SQL进行拼接,如果只是对每个权限挨个写一个SQL的话,谁都会。其难点其实主要在于如何在不污染业务代码的情况下完成SQL的拼接,Jeecg通过切面 + MyBatisPlus的QueryWrapper进行实现了这样的效果。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%