数据权限-yudao
|Word count:2.8k|Reading time:10min|Post View:
数据权限相关代码在 yudao-framework/yudao-spring-boot-starter-biz-data-permission
模块中。
类 |
说明 |
功能 |
DataPermissionDatabaseInterceptor |
数据权限 sql 拦截器 |
拦截所有 sql,追加数据权限过滤条件 |
DataPermission |
数据权限注解 |
默认已开启数据权限,可用在类或者方法上 |
DataScopeEnum |
数据范围枚举类 |
指定有哪些数据权限类型 |
DataPermissionRule |
数据权限规则接口 |
定义数据权限规则的类都应继承该接口 |
DeptDataPermissionRule |
基于部门的数据权限规则实现 |
生成数据过滤语句 |
DataPermissionRuleFactoryImpl |
工厂类 |
提供已定义的所有规则 |
DataPermissionConfiguration |
system 模块的数据权限 Configuration |
为DeptDataPermissionRule设置表名与字段 |
项目中定义了5种类型的数据权限
1 2 3 4 5 6 7 8 9 10
| public enum DataScopeEnum { ALL(1), DEPT_CUSTOM(2), DEPT_ONLY(3), DEPT_AND_CHILD(4), SELF(5); private final Integer scope;
}
|
其中的指定部门数据权限,可在前端页面中指定涉及哪些部门,可设置多个此类角色。
定义的Bean
先来看看向容器中注册的与数据权限相关的实例。
DeptDataPermissionRule
配置类 YudaoDeptDataPermissionAutoConfiguration
将一个 DeptDataPermissionRule
类注册进了容器中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
@AutoConfiguration @ConditionalOnClass(LoginUser.class) @ConditionalOnBean(value = {PermissionApi.class, DeptDataPermissionRuleCustomizer.class}) public class YudaoDeptDataPermissionAutoConfiguration {
@Bean public DeptDataPermissionRule deptDataPermissionRule(PermissionApi permissionApi, List<DeptDataPermissionRuleCustomizer> customizers) { DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi); customizers.forEach(customizer -> customizer.customize(rule)); return rule; } }
|
DeptDataPermissionRule
类定义了基于部门的数据权限规则,数据过滤条件就是由它生成的。
首先实例化了一个 DeptDataPermissionRule
,然后给某些属性赋值,经过 customizers.forEach(customizer -> customizer.customize(rule));
之后的 DeptDataPermissionRule
对象如下:
参数 customizers
是一组 DeptDataPermissionRuleCustomizer
类型的对象,由容器自动注入。DeptDataPermissionRuleCustomizer
是一个函数式接口,为了配合lambda表达式。
该接口的目的在于向map类型的属性 deptColumns
和 userColumns
添加元素,指明做数据过滤所需要的表与字段。
比如基于部门的数据权限规则,最后查询出数据之后,要根据部门id对数据进一步过滤(WHERE 表.字段 IN(?, ?, …))。因此需要指明哪一个表是部门表,所依据的表字段是哪一个。yudao的部门表名为system_dept,字段名为dept_id。
对应于ruoyi-plus,是@DataPermission注解,该注解指明了表的字段。
1 2 3 4 5 6 7 8
| @DataPermission({ @DataColumn(key = "deptName", value = "dept_id") //key值是索引,为了得到value值,不是为了指明表名 }) List`<SysDept>` selectDeptList(@Param(Constants.WRAPPER) Wrapper`<SysDept>` queryWrapper);
DEPT_AND_CHILD("4", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} )", ""),
|
对应到ruoyi中,是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Override @DataScope(deptAlias = "d", userAlias = "u") public List<SysUser> selectUserList(SysUser user) { return userMapper.selectUserList(user); } ... 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())); }
|
yudao中,容器只有一个DeptDataPermissionRuleCustomizer类型的对象,由system模块下的配置类DataPermissionConfiguration实例化并注册进容器。
1 2 3 4 5 6 7 8 9 10
| @Bean public DeptDataPermissionRuleCustomizer sysDeptDataPermissionRuleCustomizer() { return rule -> { rule.addDeptColumn(AdminUserDO.class); rule.addDeptColumn(DeptDO.class, "id"); rule.addUserColumn(AdminUserDO.class, "id"); }; }
|
这个lambda对应上面的customizer.customize(rule)的实现,可以看到调用了addDeptColumn
和addUserColumn,这两个函数会分别向map类型的属性deptColumns和userColumns添加元素,结果为:
yudao是基于表做过滤条件的,比如查询中语句中涉及了system_dept和system_users两个表,则会依次遍历这两个表,生成过滤条件。对于表system_dept,遍历所有的定义的数据权限规则,yudao中只有一个,即DeptDataPermissionRuleCustomizer,然后看这个类中是否涉及到表system_dept,如果涉及,则deptColumns.get(system_dept)获取表中的部门字段、userColumns.get(system_dept)获取表中的用户字段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| protected Expression builderExpression(Expression currentExpression, List<Table> tables) { ... for (Table table : tables) { for (DataPermissionRule rule : ContextHolder.getRules()) { if (rule.getTableNames().contains(table.getName())) { String tableName = MyBatisUtils.getTableName(table); Expression deptExpression = buildDeptExpression(tableName, table.getAlias(), deptDataPermission.getDeptIds()); Expression userExpression = buildUserExpression(tableName, table.getAlias() }
} ... }
|
DataPermissionRuleFactory
DataPermissionRuleFactory
类保存了所有的规则,可以通过它的函数 getDataPermissionRule
来获取规则。yudao中只定义了一个规则,即DeptDataPermissionRule。
1 2 3 4 5
| @Bean public DataPermissionRuleFactory dataPermissionRuleFactory(List<DataPermissionRule> rules) { return new DataPermissionRuleFactoryImpl(rules); }
|
逻辑流程
以获取部门列表为例,前端发送请求 /system/dept/list
至后端,后端在执行到对应的sql语句查询数据库之前,会调用 DataPermissionDatabaseInterceptor
的 beforeQuery
方法。DataPermissionDatabaseInterceptor
是项目定义的数据权限拦截器,继承了 JsqlParserSupport
和 InnerInterceptor
,由于其注册到了mybatis plus拦截链中,因此会拦截要执行的sql语句。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Bean public DataPermissionRuleFactory dataPermissionRuleFactory(List<DataPermissionRule> rules) { return new DataPermissionRuleFactoryImpl(rules); }
@Bean public DataPermissionDatabaseInterceptor dataPermissionDatabaseInterceptor(...){ DataPermissionDatabaseInterceptor inner = new DataPermissionDatabaseInterceptor(ruleFactory); MyBatisUtils.addInterceptor(interceptor, inner, 0); return inner; }
|
1 2 3 4 5 6 7 8
| @Override public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(ms.getId()); ... }
|
获取部门列表的例子中,beforeQuery
的参数 ms
值为
语句 ruleFactory.getDataPermissionRule(ms.getId())
会获取所有定义的数据权限规则,获取的 rules
为
由于是查询语句,会自动调用 DataPermissionDatabaseInterceptor::processSelect
方法。 DataPermissionDatabaseInterceptor::builderExpression
方法会返回数据的过滤条件,然后追加到原有的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
|
protected Expression builderExpression(Expression currentExpression, List<Table> tables) { if (CollectionUtils.isEmpty(tables)) { return currentExpression; }
Expression dataPermissionExpression = null; for (Table table : tables) { Expression expression = buildDataPermissionExpression(table); if (expression == null) { continue; } dataPermissionExpression = dataPermissionExpression == null ? expression : new AndExpression(dataPermissionExpression, expression); }
if (dataPermissionExpression == null) { return currentExpression; } if (currentExpression == null) { return dataPermissionExpression; } if (currentExpression instanceof OrExpression) { return new AndExpression(new Parenthesis(currentExpression), dataPermissionExpression); } return new AndExpression(currentExpression, dataPermissionExpression); }
|
DataPermissionDatabaseInterceptor::buildDataPermissionExpression
根据指定表构建过滤条件,核心逻辑在 DeptDataPermissionRule::getExpression
中。
1 2 3 4 5 6 7 8 9 10 11
| public Expression getExpression(String tableName, Alias tableAlias) { ... deptDataPermission = permissionApi.getDeptDataPermission(loginUser.getId()); ... Expression deptExpression = buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds()); Expression userExpression = buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId()); ... }
|
其中 permissionApi.getDeptDataPermission(loginUser.getId());
根据用户id获取能看到的数据
1 2 3 4 5 6
| @Data public class DeptDataPermissionRespDTO { private Boolean all; private Boolean self; private Set<Long> deptIds; }
|
如果用户拥有“查看自己“权限,则字段self的值为true。一个拥有“指定部门权限”和“部门及部门以下权限”的用户的一个DeptDataPermissionRespDTO实例可能为:
之后会根据self和deptIds的值生成过滤条件。
函数buildDeptExpression根据deptIds生成部门的过滤条件
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| private Expression buildDeptExpression(String tableName, Alias tableAlias, Set<Long> deptIds) { String columnName = deptColumns.get(tableName); if (StrUtil.isEmpty(columnName)) { return null; } if (CollUtil.isEmpty(deptIds)) { return null; } return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new ExpressionList(CollectionUtils.convertList(deptIds, LongValue::new))); }
|
如何使用
对于部门过滤条件语句“{表}.{字段} IN (部门1id, 部门2id, 部门3id)”和基于用户的过滤条件语句”{表}.{字段} = {userId}”,yudao已提供表达式右侧的部分,左侧部分需要自己提供。ruoyi和ruoyi-plus中,使用了注解来指明表与字段,yudao中是通过类DataPermissionConfiguration来配置。
1 2 3 4 5 6 7 8 9 10
| @Bean public DeptDataPermissionRuleCustomizer sysDeptDataPermissionRuleCustomizer() { return rule -> { rule.addDeptColumn(AdminUserDO.class); rule.addDeptColumn(DeptDO.class, "id"); rule.addUserColumn(AdminUserDO.class, "id"); }; }
|
只需要在lambda表达式中添加语句即可。
要对某个表的数据进行基于部门过滤,添加rule.addDeptColumn({表名}, {表字段});
比如公司的项目依托于部门,项目表system_project中有字段deptttt_id指明该项目隶属于哪个部门,该表对应的映射类为ProjectDo,如果要满足基于部门过滤功能,可以在上面的lambda表达式中添加rule.addDeptColumn(ProjectDo.class, “deptttt_id”);
如果要满足基于本人的数据过滤功能,如果表中有人员id字段,则直接添加rule.addUserColumn({表名}, {表字段});即可。如果没有,比如项目表和人员表是多对多的关系(一个人可以参与到多个项目,一个项目由多个人参与),就需要在查询项目时,连接(join)人员表system_user,这样就有了字段可以与表达式右侧的userid做比较,生成“system_user.userid = {userid};”过滤语句。如果不是system_user,是其他的可以与userid做比较的表,需要添加规则rule.addDeptColumn(表.class, 字段);
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
| protected void processPlainSelect(PlainSelect plainSelect) { ... List<Table> mainTables = new ArrayList<>(list); List<Join> joins = plainSelect.getJoins(); if (CollectionUtils.isNotEmpty(joins)) { mainTables = processJoins(mainTables, joins); } ... if (CollectionUtils.isNotEmpty(mainTables)) { plainSelect.setWhere(builderExpression(where, mainTables)); } }
protected Expression builderExpression(Expression currentExpression, List<Table> tables) { ... for (Table table : tables) { Expression expression = buildDataPermissionExpression(table); ... } ... }
|
自定义数据权限规则
如果想要自定义规则,需要:
创建记录当前用户拥有的数据权限等级的类。可以使用现有的DeptDataPermissionRespDTO
1 2 3 4 5 6
| @Data public class DeptDataPermissionRespDTO { private Boolean all; private Boolean self; private Set<Long> deptIds; }
|
用法具体见类DeptDataPermissionRule的getExpression方法
创建规则实现类,需继承DataPermissionRul。该类的目的是提供过滤语句,可参考类DeptDataPermissionRule。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @AllArgsConstructor public class 规则实现类 implements DataPermissionRule {
private final Set<String> TABLE_NAMES = new HashSet<>();
@Override public Set<String> getTableNames() {return TABLE_NAMES;}
@Override public Expression getExpression(String tableName, Alias tableAlias) { if (权限级别1) ... else if(权限级别2) ... else if(权限级别3) ... else ... } }
|
创建一个配置类,将规则实现类注册为bean。也可以在现有的配置类YudaoDeptDataPermissionAutoConfiguration中注册。注册时应初始化规则实现类的TABLE_NAMES属性,指明该规则应作用到那些表。在查询覆盖的表时,才会应用该规则。
1 2 3 4 5 6 7 8 9
| @Bean public 规则实现类 foo(PermissionApi permissionApi) { 规则实现类 rule = new 规则实现类(permissionApi); rule.TABLE_NAMES.add(表1); rule.TABLE_NAMES.add(表2); ... return rule; }
|