数据权限相关代码在 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;

}

其中的指定部门数据权限,可在前端页面中指定涉及哪些部门,可设置多个此类角色。
img

定义的Bean

先来看看向容器中注册的与数据权限相关的实例。

DeptDataPermissionRule

配置类 YudaoDeptDataPermissionAutoConfiguration将一个 DeptDataPermissionRule类注册进了容器中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 基于部门的数据权限 AutoConfiguration
*/
@AutoConfiguration
@ConditionalOnClass(LoginUser.class)
@ConditionalOnBean(value = {PermissionApi.class, DeptDataPermissionRuleCustomizer.class})
public class YudaoDeptDataPermissionAutoConfiguration {

@Bean
public DeptDataPermissionRule deptDataPermissionRule(PermissionApi permissionApi,
List<DeptDataPermissionRuleCustomizer> customizers) {
// 创建 DeptDataPermissionRule 对象
DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi);
// 补全表配置
customizers.forEach(customizer -> customizer.customize(rule));
return rule;
}
}

DeptDataPermissionRule类定义了基于部门的数据权限规则,数据过滤条件就是由它生成的。

首先实例化了一个 DeptDataPermissionRule,然后给某些属性赋值,经过 customizers.forEach(customizer -> customizer.customize(rule));之后的 DeptDataPermissionRule对象如下:
img
参数 customizers是一组 DeptDataPermissionRuleCustomizer类型的对象,由容器自动注入。DeptDataPermissionRuleCustomizer是一个函数式接口,为了配合lambda表达式。
该接口的目的在于向map类型的属性 deptColumnsuserColumns添加元素,指明做数据过滤所需要的表与字段。
比如基于部门的数据权限规则,最后查询出数据之后,要根据部门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);

//ruoyi-plus中的部门及以下数据权限
//将"#{#deptName}"替换为"dept_id"
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
//SysUserServiceImpl::selectUserList
@Override
@DataScope(deptAlias = "d", userAlias = "u") //相当于yudao中的键,指明哪个表。
public List<SysUser> selectUserList(SysUser user)
{
return userMapper.selectUserList(user);
}
...
else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
{
//dept_id指明了字段。
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 -> {
// dept
rule.addDeptColumn(AdminUserDO.class);//类 `AdminUserDO`是表system_dept的映射类
rule.addDeptColumn(DeptDO.class, "id");//`DeptDO`是表system_users的映射类。
// user
rule.addUserColumn(AdminUserDO.class, "id");
};
}

这个lambda对应上面的customizer.customize(rule)的实现,可以看到调用了addDeptColumn
和addUserColumn,这两个函数会分别向map类型的属性deptColumns和userColumns添加元素,结果为:
img

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
//DataPermissionDatabaseInterceptor
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
//YudaoDataPermissionAutoConfiguration
@Bean
public DataPermissionRuleFactory dataPermissionRuleFactory(List<DataPermissionRule> rules) {
return new DataPermissionRuleFactoryImpl(rules);
}

逻辑流程

以获取部门列表为例,前端发送请求 /system/dept/list至后端,后端在执行到对应的sql语句查询数据库之前,会调用 DataPermissionDatabaseInterceptorbeforeQuery方法。DataPermissionDatabaseInterceptor是项目定义的数据权限拦截器,继承了 JsqlParserSupportInnerInterceptor,由于其注册到了mybatis plus拦截链中,因此会拦截要执行的sql语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//YudaoDataPermissionAutoConfiguration
@Bean
public DataPermissionRuleFactory dataPermissionRuleFactory(List<DataPermissionRule> rules) {
return new DataPermissionRuleFactoryImpl(rules);
}

@Bean
public DataPermissionDatabaseInterceptor dataPermissionDatabaseInterceptor(...){
// 创建 DataPermissionDatabaseInterceptor 拦截器
DataPermissionDatabaseInterceptor inner = new DataPermissionDatabaseInterceptor(ruleFactory);
// 添加到 interceptor 中
// 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
MyBatisUtils.addInterceptor(interceptor, inner, 0);
return inner;
}
1
2
3
4
5
6
7
8
//DataPermissionDatabaseInterceptor
@Override // SELECT 场景
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 获得 Mapper 对应的数据权限的规则
List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(ms.getId());
...
}

获取部门列表的例子中,beforeQuery的参数 ms值为
img
语句 ruleFactory.getDataPermissionRule(ms.getId())会获取所有定义的数据权限规则,获取的 rules
img
由于是查询语句,会自动调用 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
/**
* 处理条件
*
* @param currentExpression 当前 where 条件
* @param tables 多个表
*/
protected Expression builderExpression(Expression currentExpression, List<Table> tables) {
// 没有表需要处理直接返回
if (CollectionUtils.isEmpty(tables)) {
return currentExpression;
}

// 第一步,获得 Table 对应的数据权限条件
Expression dataPermissionExpression = null;
for (Table table : tables) {
// 构建每个表的权限 Expression 条件
Expression expression = buildDataPermissionExpression(table);//!!!!
if (expression == null) {
continue;
}
// 合并到 dataPermissionExpression 中
dataPermissionExpression = dataPermissionExpression == null ? expression
: new AndExpression(dataPermissionExpression, expression);
}

// 第二步,合并多个 Expression 条件
if (dataPermissionExpression == null) {
return currentExpression;
}
if (currentExpression == null) {
return dataPermissionExpression;
}
// ① 如果表达式为 Or,则需要 (currentExpression) AND dataPermissionExpression
if (currentExpression instanceof OrExpression) {
return new AndExpression(new Parenthesis(currentExpression), dataPermissionExpression);
}
// ② 如果表达式为 And,则直接返回 where AND dataPermissionExpression
return new AndExpression(currentExpression, dataPermissionExpression);
}

DataPermissionDatabaseInterceptor::buildDataPermissionExpression根据指定表构建过滤条件,核心逻辑在 DeptDataPermissionRule::getExpression中。

1
2
3
4
5
6
7
8
9
10
11
//DeptDataPermissionRule类
public Expression getExpression(String tableName, Alias tableAlias) {
...
// 获得数据权限
deptDataPermission = permissionApi.getDeptDataPermission(loginUser.getId());
...
// 拼接 Dept 和 User 的条件,最后组合
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 -> {
// dept
rule.addDeptColumn(AdminUserDO.class);
rule.addDeptColumn(DeptDO.class, "id");
// user
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
//DataPermissionDatabaseInterceptor
protected void processPlainSelect(PlainSelect plainSelect) {
//处理where等查询
...
List<Table> mainTables = new ArrayList<>(list); //添加当前表

// 处理 join
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) {
...
// 构建每个表的权限 Expression 条件
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 {
    /**
    * 记录该规则涉及的所有表名,只有在查询覆盖的某个表时,才会调用该规则的getExpression方法
    */
    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;
    }