前言:
在使用如若依、Jeecg等后台管理系统进行二次开发的时候,我们总会涉及到数据隔离相关的内容,如每个非管理员用户应该都只能看到自己创建的数据,而不是所有的数据,本文将以jeecg为例像大家介绍如何每个用户之间的数据隔离的效果。
先从功能需求说起,当查询房屋列表的时候,每个房东(用户)应该只能看到自己的房屋(house)表数据,而房屋和房东的关系存放于房东(house_holder)表中,所以我们需要通过两个表的关联来实现数据隔离的查询。而jeecg中要如何实现呢?我们一起啦看看吧
基于Jeecg实现:
先放出官方文档中跟数据隔离有关的内容:http://doc.jeecg.com/2044046
- 进入菜单管理页面,找到想要进行数据隔离的菜单,点击添加一个下级并进行配置
选择按钮/权限并配置菜单路径(笔者能力有限暂时无法弄清菜单路径和授权表示的作用在哪)
然后对添加的这个按钮/权限进行数据规则的配置,
由于我们需要要从另外一张表中去查出跟user_id
有关的house_id
所以要使用自定义SQL的方式
- 在角色管理中对用角色进行数据权限的授权
给需求中的房东用户配置上数据权限
- 在后端对应的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
...
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的新增的属性赋值
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值
...
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
...
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
的切面
在切面中查询出了对应用户的数据权限模型(PermissionDataRuleModel),并且于当前用户的userInfo都放入了request请求中。
数据权限模型的值如下,可以看到,其值就是我们刚刚在后台中配置的数据规则
而JeecgDataAuthorUtils.installUserInfo的代码如下:
其将userinfo存放在了request当中(setter),但该切面到此处就结束了。 我们再看看整个进入Controller的list方法:
/**
* 分页列表查询
*
* @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,所以数据的隔离会不会是在这里实现的呢? 我们在进去看看,
再进入这里installMplus方法:
看到这,我们应该就找到了jeecg中居于mybatis plus实现的数据权限/隔离的代码所在地啦,但这个类十分的冗余判断,难以阅读,所以作者给出几个该类中和数据权限有关的代码:
installMplus方法中通过我们之前在切面中传入的SysPermissionDataRuleModel
开始解析我们自定义的SQL语句中的模板变量。
这个getSqlRuleValue方法就是模板替换的核心啦
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变量如下
JwtUtil中的getSystemData的代码如下:其实就是一大堆的if-else + contstant常量罢了
/**
* 从当前用户中获取变量
* @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进行实现了这样的效果。