我们在之前已经分析过该项目的搭建和一部分功能实测体验
该项目历史文章:
快速开发平台 ruoyi-vue-pro(1)- 项目搭建和功能体验
快速开发平台 ruoyi-vue-pro(2)工作流引擎模块
快速开发平台 ruoyi-vue-pro (3) - 账号授权体系实测分析
快速开发平台 ruoyi-vue-pro(4)- 商城移动端功能实测
快速开发平台 ruoyi-vue-pro(5)支付中心功能设计分析和实测
今天我们来看一下项目中的数据权限功能是怎么用的,以及它的技术设计思路
- 数据权限功能测试
首先我们可以在角色管理中针对角色进行数据权限的维护,可以看到,数据权限是基于 角色+部门 来实现的,权限范围也是围绕着用户自身和所在的部门来设计的,我接下来定义两个角色来测试一下。
增加了两个角色,一个普通的,只能看自己的数据;另一个是小组长,可以看本部门以及下级部门的数据
我们把两个角色都开通“用户管理”这个功能菜单,现在我们试试用test1普通员工的权限登录一下
可以看到在这个菜单里,test1普通员工的账号因为数据权限是仅限自己的数据,所以只能看到自己。
然后使用test2账号进行登录,发现如数据权限范围所设定的一样,他可以看到研发部门下的所有数据。
其余的数据权限范围我就不逐一去测试了,其实最主要用的就是这种场景。
- 技术原理分析
首先我们找到数据权限模块的底层工程,进入它的配置类从而开始分析
其主要定义了3个Bean。
- DataPermissionRuleFactory
数据权限规则工厂,用来将项目中的数据权限规则的操作做成对外服务的统一出口,可以看到这个工厂接口定义了2个主要方法,即“获得所有数据权限规则数组”,“获得指定 Mapper 的数据权限规则数组”。这里的Mapper其实就是mybatis的dao接口,数据权限最终的实现是要基于数据库查询的操作类,给它的sql注入数据权限过滤的片段。
目前项目中只实现了一个默认的DataPermissionRuleFactoryImpl,其实可以理解成就是这个工厂就是一个util类而已,它这里的工厂模式设计是便于后面的扩展,比如具体的Rule配置可以直接从数据库获取,这样只需要新写一个FactoryImpl即可。
- DataPermissionDatabaseInterceptor
这个是数据权限拦截器,可以从上面的图中看到,具体这个Bean内部其实就是增加了一个Mybatis的拦截器
这个拦截器实现了Mybatis plus的 sql解析支持类和拦截接口,另外可以看到,它需要将上面定义的DataPermissionRuleFactory注入进来,其实也就是DataPermissionRuleFactoryImpl
- DataPermissionAnnotationAdvisor
基于spring aop的拦截切面设置,可以看到它这里主要是拦截 @DataPermission 的类和方法,最后合并起来形成一个切面
上面分析了数据权限模块的第一个配置类,它其实还有第二个
这个是基于部门的数据权限相关的配置,它主要是将项目工程里所有的DeptDataPermissionRuleCustomizer实现类抓取到,然后将权限API操作类注入到一个实例化的rule,最后将rule进行自定义操作方法调用后返回。
说简单点,就是把rule对象丢到项目里每个自定义的DeptDataPermissionRuleCustomizer里去customize
搜索一下,项目里的system工程,就自定义了一个,放在数据权限实现里去理解,它这里就是配置了 system_users 表需要按照dept_id去实现部门权限控制;而system_dept 表给定了字段,那就直接按照id来控制
可以看出来,整个数据权限控制细化到表和字段级别的配置,是直接在代码里控制的!而界面上能操作的只是给角色定义好 部门权限范围
配置的部分看完了,接下来看看核心拦截过程!
数据权限拦截的核心主要集中在DataPermissionDatabaseInterceptor
可以大致分为 before builder process 三个部分,其中before就是在查询或者预处理阶段针对规则引擎进行初始化等操作;而builder是将配置好的具体数据权限规则转化为具体按表按字段的sql片段,process就是具体的执行过程,其内部也是调用到builder部分。
整体可以理解为是一个sql拆解重新拼装的过程,首先利用mybatis的sql解析插件进行sql拆解,然后在条件拼装过程中加载到数据权限的配置,然后进行sql条件重写。我们可以重点看下这个方法
这里先是从上下文中拿到配置的rules(在之前的before 里初始化的),然后从table对应的Rule里去获取表达式getExpression,而具体的Expression表达式是我们之前看到的Rule的实现类DeptDataPermissionRule里定义的
public Expression getExpression(String tableName, Alias tableAlias) {
// 只有有登陆用户的情况下,才进行数据权限的处理
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
if (loginUser == null) {
return null;
}
// 只有管理员类型的用户,才进行数据权限的处理
if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) {
return null;
}
// 获得数据权限
DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class);
// 从上下文中拿不到,则调用逻辑进行获取
if (deptDataPermission == null) {
deptDataPermission = permissionApi.getDeptDataPermission(loginUser.getId());
if (deptDataPermission == null) {
log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser));
throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限",
loginUser.getId(), tableName, tableAlias.getName()));
}
// 添加到上下文中,避免重复计算
loginUser.setContext(CONTEXT_KEY, deptDataPermission);
}
// 情况一,如果是 ALL 可查看全部,则无需拼接条件
if (deptDataPermission.getAll()) {
return null;
}
// 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限
if (CollUtil.isEmpty(deptDataPermission.getDeptIds())
&& Boolean.FALSE.equals(deptDataPermission.getSelf())) {
return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空
}
// 情况三,拼接 Dept 和 User 的条件,最后组合
Expression deptExpression = buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds());
Expression userExpression = buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId());
if (deptExpression == null && userExpression == null) {
// TODO 芋艿:获得不到条件的时候,暂时不抛出异常,而是不返回数据
log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]",
JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission));
// throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空",
// loginUser.getId(), tableName, tableAlias.getName()));
return EXPRESSION_NULL;
}
if (deptExpression == null) {
return userExpression;
}
if (userExpression == null) {
return deptExpression;
}
// 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE (dept_id IN ? OR user_id = ?)
return new Parenthesis(new OrExpression(deptExpression, userExpression));
}
可以看出来,在获取表达式过程中,规则里加载了用户登录信息、权限配置,最终形成了一个实际的sql过滤条件。
最后的最后,就是把过滤条件组装好后重写SQL,切到Mybatis执行SQL的前夕,让它执行重写后的叠加了数据权限的SQL。
至于SQL解析的细节,我们就不赘述了,可以去参考下sql解析插件的机制。
- 总结
我们本篇小小测试了一下项目中的数据权限功能。
1、系统支持在角色中配置 用户&部门级别的数据权限范围,然后系统的控制器中,追加 @DataPermission 注解(其实不加也可以,默认就是打开的,它这里是反选逻辑,如果加了并且参数里进行了禁用,则就表示不拦截),就能实现数据的拦截权限
2、设计上还不太完善的点:
如果需要去细化控制相同角色在不同功能上的数据权限是做不到的
数据权限控制涉及的表以及字段需要在代码里去配置
3、总体来说技术设计上还是比较优雅的,可扩展性也比较强