1. 数据权限控制的本质与MyBatis-Plus的定位
数据权限控制是每个企业级应用都无法绕开的核心需求。想象这样一个场景:一家全国连锁零售企业的区域经理登录系统时,只能看到自己管辖区域内门店的销售数据;而门店店长则只能查看本店员工信息。这种基于组织架构、角色、甚至业务属性的数据过滤需求,就是典型的数据权限控制场景。
与传统的功能权限(菜单权限、按钮权限)不同,数据权限的复杂性体现在三个维度:
- 动态性:过滤条件会随用户身份实时变化
- 透明性:对业务代码应尽可能无侵入
- 叠加性:多种权限规则可能需要组合生效
MyBatis-Plus作为MyBatis的增强工具包,其核心价值正是简化CRUD操作。但在3.4.0版本后,通过DataPermissionInterceptor的引入,它提供了一套优雅的数据权限解决方案。与Sharding-JDBC等方案相比,MyBatis-Plus的优势在于:
- 零学习成本(对已有MyBatis-Plus用户)
- 与MyBatis原生功能无缝集成
- 支持注解和编程式两种配置方式
关键理解:数据权限的本质是在SQL执行前动态修改查询条件,这与MyBatis的插件机制完美契合。MyBatis-Plus的拦截器正是在
StatementHandler层面进行SQL重写。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心拦截器机制深度拆解
2.1 DataPermissionInterceptor工作原理
当我们在Spring Boot项目中引入mybatis-plus-boot-starter时,以下拦截器会自动生效:
java复制@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class DataPermissionInterceptor implements Interceptor {
// 核心处理逻辑
}
这个拦截器的执行时机非常关键——它发生在SQL语句被JDBC预处理之前,但已经在MyBatis完成了#{}参数替换之后。这意味着我们可以安全地修改SQL而不用担心参数绑定问题。
拦截器的核心处理流程:
- 通过反射获取MappedStatement和BoundSql
- 解析方法上的@DataPermission注解
- 根据当前用户上下文构建WHERE条件片段
- 使用JSqlParser重写原始SQL
2.2 SQL重写背后的黑科技
MyBatis-Plus采用JSqlParser这个强大的SQL解析库来实现AST级别的SQL修改。以下是一个实际的重写示例:
原始SQL:
sql复制SELECT * FROM sales_order
当部门经理访问时,拦截器会将其重写为:
sql复制SELECT * FROM sales_order WHERE dept_id = 10086
更复杂的情况下,如果存在多个权限规则(比如既要限制部门又要限制订单类型),生成的SQL可能是:
sql复制SELECT * FROM sales_order
WHERE dept_id = 10086
AND order_type IN ('RETAIL','WHOLESALE')
踩坑警示:JSqlParser对某些特殊语法支持有限,比如WITH子句。在实际项目中建议先用测试SQL验证解析兼容性。
3. 实战:从零构建数据权限系统
3.1 基础环境搭建
首先确保依赖版本正确(以2023年最新版本为例):
xml复制<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>4.5</version>
</dependency>
Spring Boot配置类需要显式声明拦截器(虽然3.4.0+版本会自动注册,但显式声明更可控):
java复制@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new DataPermissionInterceptor(
new MyDataPermissionHandler()
));
return interceptor;
}
3.2 权限规则定义实战
我们通过实现DataPermissionHandler接口来定义具体规则:
java复制public class MyDataPermissionHandler implements DataPermissionHandler {
@Override
public Expression getSqlSegment(Expression where, String mappedStatementId) {
// 获取当前用户上下文
LoginUser user = SecurityUtils.getLoginUser();
// 构建部门过滤条件
EqualsTo deptFilter = new EqualsTo(
new Column("dept_id"),
new LongValue(user.getDeptId())
);
// 如果是查询敏感表,追加额外条件
if (mappedStatementId.contains("Salary")) {
EqualsTo userFilter = new EqualsTo(
new Column("user_id"),
new LongValue(user.getUserId())
);
return new AndExpression(deptFilter, userFilter);
}
return deptFilter;
}
}
3.3 注解驱动的高级用法
对于需要灵活控制的场景,可以使用@DataPermission注解:
java复制@DataPermission({
@DataColumn(key = "dept_id", value = "#user.deptId"),
@DataColumn(key = "project_id", value = "#user.projectId")
})
@Select("SELECT * FROM project_task")
List<Task> selectTasks();
注解支持SpEL表达式,可以实现动态值注入。比如当需要实现"本人数据+本部门数据"这种复杂逻辑时:
java复制@DataPermission(
value = @DataColumn(key = "owner", value = "#user.userId"),
or = @DataColumn(key = "dept_id", value = "#user.deptId")
)
4. 生产级优化与疑难解答
4.1 性能调优方案
在大数据量场景下,不当的数据权限实现可能导致严重性能问题。以下是经过验证的优化策略:
-
索引检查清单:
- 确保所有作为过滤条件的字段都有合适索引
- 多条件组合时考虑复合索引顺序
- 定期执行
EXPLAIN验证执行计划
-
缓存策略:
java复制// 使用Spring Cache缓存权限SQL片段
@Cacheable(value = "dataPermission", key = "#mappedStatementId")
public Expression getSqlSegment(Expression where, String mappedStatementId) {
// ...
}
- 批量操作特殊处理:
java复制// 在DataPermissionHandler中识别批量操作
if (mappedStatementId.contains("BatchUpdate")) {
return null; // 跳过权限过滤
}
4.2 典型问题排查指南
问题一:分页总数错误
现象:分页查询的total与实际可见数据量不符
解决方案:
java复制// 在自定义handler中处理COUNT查询
if (mappedStatementId.endsWith("_COUNT")) {
return new Parenthesis(where);
}
问题二:多表关联失效
现象:JOIN查询时权限条件被错误应用
修复方案:
java复制// 明确指定表别名
new EqualsTo(
new Column("t.dept_id"),
new LongValue(user.getDeptId())
)
问题三:SQL注入风险
当使用字符串拼接方式构造条件时:
java复制// 错误做法!存在注入风险
String sql = "dept_id = " + user.getDeptId();
// 正确做法:使用参数化构造
EqualsTo eq = new EqualsTo(
new Column("dept_id"),
new LongValue(user.getDeptId())
);
4.3 与租户隔离的协同方案
当项目同时需要多租户隔离和数据权限时,处理优先级很重要:
- 租户过滤(TenantLineInnerInterceptor)
- 数据权限过滤(DataPermissionInterceptor)
- 其他拦截器(如分页、乐观锁)
配置示例:
java复制interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(...));
interceptor.addInnerInterceptor(new DataPermissionInterceptor(...));
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(...));
5. 扩展进阶:动态权限策略
对于需要运行时修改权限规则的场景(如临时授权),可以采用策略模式:
java复制public class DynamicPermissionHandler implements DataPermissionHandler {
private final Map<String, PermissionStrategy> strategies;
@Override
public Expression getSqlSegment(Expression where, String mappedStatementId) {
String strategyKey = SecurityContext.getCurrentStrategy();
return strategies.get(strategyKey).buildExpression(where);
}
}
interface PermissionStrategy {
Expression buildExpression(Expression origin);
}
与Spring Security的深度集成方案:
java复制@PreAuthorize("hasPermission(#id, 'sales_order', 'read')")
@DataPermission(handler = SalesDataPermissionHandler.class)
Order getOrderById(Long id);
在实际项目中,我们发现数据权限往往需要与工作流引擎配合。比如当审批流程流转到某个环节时,自动赋予相关人员数据访问权限。这种场景下,可以结合Activiti等引擎的TaskListener动态更新权限上下文。
最后分享一个真实案例中的技巧:对于超级管理员角色,可以通过在handler中快速返回null来跳过所有过滤,避免不必要的性能开销:
java复制if (user.isSuperAdmin()) {
return null; // 不过滤任何数据
}
