在MyBatis plus的配置类中,第一个拦截器是 PlusDataPermissionInterceptor

1
2
3
4
5
6
7
8
9
10
11
12
public class MybatisPlusConfig {

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 数据权限拦截器,需要加在首位,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
interceptor.addInnerInterceptor(new PlusDataPermissionInterceptor());
... // 其他拦截器
return interceptor;
}
}

这是项目定义的数据权限拦截器,用于拦截所有的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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public class PlusDataPermissionInterceptor extends JsqlParserSupport implements InnerInterceptor {

private final PlusDataPermissionHandler dataPermissionHandler = new PlusDataPermissionHandler();

//操作前置处理
@Override // SELECT 场景
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
// 检查忽略注解
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
return;
}
// 检查是否无效 无数据权限注解
if (dataPermissionHandler.isInvalid(ms.getId())) {
return;
}
// 解析 sql 分配对应方法
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
}
//操作前置处理
@Override // 只处理 UPDATE / DELETE 场景,不处理 INSERT 场景(因为 INSERT 不需要数据权限)
public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
MappedStatement ms = mpSh.mappedStatement();
SqlCommandType sct = ms.getSqlCommandType();
if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
return;
}
PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
mpBs.sql(parserMulti(mpBs.sql(), ms.getId()));
}
}
//处理查询
@Override
protected void processSelect(Select select, int index, String sql, Object obj) {
SelectBody selectBody = select.getSelectBody();
if (selectBody instanceof PlainSelect) {
this.setWhere((PlainSelect) selectBody, (String) obj);
} else if (selectBody instanceof SetOperationList) {
SetOperationList setOperationList = (SetOperationList) selectBody;
List<SelectBody> selectBodyList = setOperationList.getSelects();
selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
}
}

@Override
protected void processUpdate(Update update, int index, String sql, Object obj) {
Expression sqlSegment = dataPermissionHandler.getSqlSegment(update.getWhere(), (String) obj, false);
if (null != sqlSegment) {
update.setWhere(sqlSegment);
}
}

@Override
protected void processDelete(Delete delete, int index, String sql, Object obj) {
Expression sqlSegment = dataPermissionHandler.getSqlSegment(delete.getWhere(), (String) obj, false);
if (null != sqlSegment) {
delete.setWhere(sqlSegment);
}
}

/**
* 设置 where 条件
*
* @param plainSelect 查询对象
* @param mappedStatementId 执行方法id
*/
protected void setWhere(PlainSelect plainSelect, String mappedStatementId) {
Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), mappedStatementId, true);
if (null != sqlSegment) {
plainSelect.setWhere(sqlSegment);
}
}

}

以一个数据权限为“4”(部门及部门以下)的用户在查询部门数据为例,最终程序会调用SysDeptMaper.java文件中的 selectDeptList方法。

1
2
3
4
@DataPermission({
@DataColumn(key = "deptName", value = "dept_id")
})
List<SysDept> selectDeptList(@Param(Constants.WRAPPER) Wrapper<SysDept> queryWrapper);

在获取Mapper实现文件中的sql语句之后,从数据库查询数据之前,会调用 beforeQuery方法,分析添加在 selectDeptList方法上的注解,然后分析sql语句。因为是查询,会调用拦截器中的 processSelect方法,该方法中又调用了 setWhere,在已有的sql语句上根据用户的数据权限追加where条件语句。
setWhere方法中调用了 PlusDataPermissionHandlergetSqlSegmentPlusDataPermissionHandler是项目定义的数据权限处理器,目的是用于在拦截的 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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
//DataPermissionHandler类
public Expression getSqlSegment(Expression where, String mappedStatementId, boolean isSelect) {
DataColumn[] dataColumns = findAnnotation(mappedStatementId);
if (ArrayUtil.isEmpty(dataColumns)) {
invalidCacheSet.add(mappedStatementId);
return where;
}
LoginUser currentUser = DataPermissionHelper.getVariable("user");
if (ObjectUtil.isNull(currentUser)) {
currentUser = LoginHelper.getLoginUser();
DataPermissionHelper.setVariable("user", currentUser);
}
// 如果是超级管理员,则不过滤数据
if (LoginHelper.isAdmin()) {
return where;
}
String dataFilterSql = buildDataFilter(dataColumns, isSelect); //非超级管理员用户,通过 buildDataFilter() 方法构造 SQL 查询条件。
if (StringUtils.isBlank(dataFilterSql)) {
return where;
}
try {
Expression expression = CCJSqlParserUtil.parseExpression(dataFilterSql);
// 数据权限使用单独的括号 防止与其他条件冲突
Parenthesis parenthesis = new Parenthesis(expression);
if (ObjectUtil.isNotNull(where)) {
return new AndExpression(where, parenthesis);
} else {
return parenthesis;
}
} catch (JSQLParserException e) {
throw new ServiceException("数据权限解析异常 => " + e.getMessage());
}
}

/**
* 构造数据过滤sql
*/
private String buildDataFilter(DataColumn[] dataColumns, boolean isSelect) {
// 更新或删除需满足所有条件
String joinStr = isSelect ? " OR " : " AND "; //获取拼接字符
LoginUser user = DataPermissionHelper.getVariable("user");
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(beanResolver);
DataPermissionHelper.getContext().forEach(context::setVariable); //从 DataPermissionHelper 获取用户信息并保存到上下文中。
Set<String> conditions = new HashSet<>();
for (RoleDTO role : user.getRoles()) { //获取用户角色信息并循环进行操作
user.setRoleId(role.getRoleId());
// 获取角色权限泛型
DataScopeType type = DataScopeType.findCode(role.getDataScope());
if (ObjectUtil.isNull(type)) {
throw new ServiceException("角色数据范围异常 => " + role.getDataScope());
}
// 全部数据权限直接返回
if (type == DataScopeType.ALL) {
return "";
}
boolean isSuccess = false;
for (DataColumn dataColumn : dataColumns) {
if (dataColumn.key().length != dataColumn.value().length) {
throw new ServiceException("角色数据范围异常 => key与value长度不匹配");
}
// 不包含 key 变量 则不处理
if (!StringUtils.containsAny(type.getSqlTemplate(),
Arrays.stream(dataColumn.key()).map(key -> "#" + key).toArray(String[]::new)
)) {
continue;
}
// 设置注解变量 key 为表达式变量 value 为变量值
for (int i = 0; i < dataColumn.key().length; i++) {
context.setVariable(dataColumn.key()[i], dataColumn.value()[i]);
}

// 解析sql模板并填充
String sql = parser.parseExpression(type.getSqlTemplate(), parserContext).getValue(context, String.class);
conditions.add(joinStr + sql);
isSuccess = true;
}
// 未处理成功则填充兜底方案
if (!isSuccess && StringUtils.isNotBlank(type.getElseSql())) {
conditions.add(joinStr + type.getElseSql());
}
}

if (CollUtil.isNotEmpty(conditions)) {
String sql = StreamUtils.join(conditions, Function.identity(), "");
return sql.substring(joinStr.length());
}
return "";
}

其中的 String sql = parser.parseExpression(type.getSqlTemplate(), parserContext).getValue(context, String.class);语句将会根据用户的数据权限生成过滤语句。type的类型为 DataScopeType,该类定义了用户可以有哪些数据权限。

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
public enum DataScopeType {
/**
* 全部数据权限
*/
ALL("1", "", ""),

/**
* 自定数据权限
*/
CUSTOM("2", " #{#deptName} IN ( #{@sdss.getRoleCustom( #user.roleId )} ) ", ""),

/**
* 部门数据权限
*/
DEPT("3", " #{#deptName} = #{#user.deptId} ", ""),

/**
* 部门及以下数据权限
*/
DEPT_AND_CHILD("4", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} )", ""),

/**
* 仅本人数据权限
*/
SELF("5", " #{#userName} = #{#user.userId} ", " 1 = 0 ");
...
}

比如用户的数据权限为4,则对应为 " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} )",其中的 #deptName#user.deptId会被 context中的值替换。
img
@sdss.getDeptAndChild()对应的是 SysDataScopeServiceImpl类中的 getDeptAndChild方法。

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
@RequiredArgsConstructor
@Service("sdss")
public class SysDataScopeServiceImpl implements ISysDataScopeService {

private final SysRoleDeptMapper roleDeptMapper;
private final SysDeptMapper deptMapper;

@Override
public String getRoleCustom(Long roleId) {
List<SysRoleDept> list = roleDeptMapper.selectList(
new LambdaQueryWrapper<SysRoleDept>()
.select(SysRoleDept::getDeptId)
.eq(SysRoleDept::getRoleId, roleId));
if (CollUtil.isNotEmpty(list)) {
return StreamUtils.join(list, rd -> Convert.toStr(rd.getDeptId()));
}
return null;
}

@Override
public String getDeptAndChild(Long deptId) {
List<SysDept> deptList = deptMapper.selectList(new LambdaQueryWrapper<SysDept>()
.select(SysDept::getDeptId)
.apply(DataBaseHelper.findInSet(deptId, "ancestors")));
List<Long> ids = StreamUtils.toList(deptList, SysDept::getDeptId);
ids.add(deptId);
List<SysDept> list = deptMapper.selectList(new LambdaQueryWrapper<SysDept>()
.select(SysDept::getDeptId)
.in(SysDept::getDeptId, ids));
if (CollUtil.isNotEmpty(list)) {
return StreamUtils.join(list, d -> Convert.toStr(d.getDeptId()));
}
return null;
}
}

项目中的数据权限实现涉及的类如下:

说明 功能
PlusDataPermissionInterceptor 数据权限 sql 拦截器 用于拦截所有 sql。 检查是否标注了 DataPermission 注解
PlusDataPermissionHandler 数据权限处理器 在拦截的 sql中添加数据权限过滤条件
SysDataScopeService 自定义 Bean 处理数据权限 用于自定义扩展
DataPermission 组注解 用于标注开启数据权限 (默认过滤部门权限)
DataColumn 具体的数据权限字段标注 用于替换数据权限模板内的 key 变量
DataScopeType 模板定义 用于定义数据权限模板
SysDataScopeService 数据权限实现 用于自定义扩展

参考:
【RuoYi-Vue-Plus】学习笔记 09 - 数据权限调用流程分析(参照 Mybatis Plus 数据权限插件)