mybatis-plus 之 权限拦截插件 DataPermissionInterceptor
MyBatis-Plus 数据权限插件原理与完整使用详解
MyBatis-Plus 数据权限插件(DataPermissionInterceptor) 是基于 MyBatis 拦截器链实现的行级数据隔离组件,底层结合 SQL 解析、AST 抽象语法树改写,自动为 SELECT/UPDATE/DELETE 语句追加权限条件,实现无代码侵入的数据权限控制(部门权限、个人数据、租户隔离、角色数据范围等)。
本文从前置知识、整体架构、核心原理、完整使用、源码流程、高级场景、避坑全维度讲解。
一、前置基础
1. 适用场景
- 按部门ID过滤:只能查看本部门/下级部门数据
- 按创建人ID过滤:只能查看自己创建的数据
- 多租户
tenantId隔离(也可配合租户插件) - 角色数据范围:全部数据/本部门/本人数据
2. 核心能力
- 拦截 MyBatis 执行的 SQL,动态追加 WHERE 权限条件
- 支持主表、多表联查、子查询、分页场景
- 支持注解、表名、字段维度配置权限规则
- 可指定 Mapper/方法/表跳过权限拦截(白名单)
3. 版本说明
MP 3.5+ 正式推出全新数据权限插件,包路径:
com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor
旧版 TenantSqlParser 仅做租户,新版 DataPermissionInterceptor 通用行级权限。
4. 依赖(SpringBoot)
<!-- MyBatis-Plus 核心 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
二、整体架构与核心组件
1. 插件整体链路
MyBatis 执行流程:
Mapper调用 → Executor → MyBatis 拦截器链 → SQL 执行 → 数据库
MP 数据权限属于 InnerInterceptor(内部拦截器),依附 MP 插件体系。
核心类关系
MybatisPlusInterceptor // MP 总插件入口(统一注册所有内部拦截器)
├─ DataPermissionInterceptor // 【数据权限主拦截器】本文核心
│ ├─ DataPermissionHandler // 开发者实现:权限规则、SQL片段、黑白名单
│ ├─ SqlParser // SQL 解析器(基于 JSqlParser 解析 AST)
│ └─ TableInfoHelper // 表元数据、实体映射
├─ TenantInnerInterceptor // 租户插件(可共存)
└─ PaginationInnerInterceptor // 分页插件(常用组合)
2. 三大核心角色
1)MybatisPlusInterceptor
MP 统一插件容器,所有内部拦截器(分页、权限、租户)都要注册到它里面。
2)DataPermissionInterceptor
数据权限主拦截器:
- 拦截
query/update/delete类型 SQL - 调用 SQL 解析器解析语句
- 调用
DataPermissionHandler获取权限规则 - 动态拼接权限条件、改写 SQL
3)DataPermissionHandler(业务扩展接口)
唯一业务接入点,由开发者实现,定义:
- 哪些表需要权限
- 权限字段(
dept_id/create_user_id) - 动态权限 SQL 片段
- 表/Mapper 方法黑白名单
三、核心实现原理(重点)
1. 整体执行流程
- 应用调用 Mapper 方法,进入 MyBatis 执行链
- 经过
MybatisPlusInterceptor,流转到DataPermissionInterceptor - 判断 SQL 类型:仅处理 SELECT / UPDATE / DELETE(INSERT 一般不拦截)
- 通过 JSqlParser 将原始 SQL 解析为 AST 抽象语法树
- 调用
DataPermissionHandler:- 判断当前表是否需要拦截、当前 Mapper 是否白名单
- 获取「表名 → 权限字段/权限SQL」规则
- 遍历 AST 中的表节点,自动拼接 AND 权限条件(自动适配表别名,解决多表联查字段冲突)
- 将改写后的 AST 重新生成可执行 SQL
- 执行改写后 SQL,实现数据权限过滤
核心关键点:基于 AST 语法树改写 SQL,而非字符串截取,兼容复杂 SQL、联表、子查询,稳定性远高于字符串拼接。
2. 为什么不用字符串替换?
- 字符串匹配无法处理:多表别名、子查询、复杂
WHERE、括号、函数 - AST 解析是标准 SQL 编译级方案,工业级稳定。
3. 多表联查处理逻辑
假设原 SQL:
SELECT * FROM sys_user u LEFT JOIN sys_dept d ON u.dept_id = d.id WHERE u.name = 'test'
- AST 识别出两张表:
sys_user(u)、sys_dept(d) - 匹配权限规则:
sys_user需要dept_id权限 - 自动使用表别名拼接条件:
AND u.dept_id IN (1001,1002) - 最终 SQL:
SELECT * FROM sys_user u LEFT JOIN sys_dept d ON u.dept_id = d.id
WHERE u.name = 'test' AND u.dept_id IN (1001,1002)
4. 与 Spring AOP 数据权限插件区别
| 维度 | MP 数据权限插件 | Spring AOP 数据权限(手写 AOP) |
|---|---|---|
| 拦截位置 | MyBatis SQL 层(全局 SQL 拦截) | 业务 Service/Mapper 方法层 |
| 实现方式 | AST 解析改写 SQL | AOP 拦截 + 手动拼接 Wrapper/条件 |
| 侵入性 | 零代码侵入,无需改写 Mapper/XML | 需结合 QueryWrapper,部分场景侵入 |
| 兼容性 | 兼容 XML、注解 Mapper、复杂 SQL | 仅对代码构造的 Wrapper 友好,XML 难处理 |
| 适用范围 | 全库 SQL 统一权限管控 | 业务层精细化控制 |
工程实践:数据库行级权限优先使用 MP 数据权限插件。
四、完整使用步骤(可直接落地)
场景说明
实现部门数据权限:
- 表:
sys_user、sys_order - 权限字段:
dept_id - 规则:当前登录用户只能查询
dept_id = 当前部门ID的数据 - 配置:
sys_dict字典表、UserMapper.getAll方法加入白名单
步骤1:导入依赖(略,前文已给)
步骤2:定义登录上下文载体
模拟当前登录用户信息(实际从 Token/ThreadLocal/TL 获取)
import com.alibaba.ttl.TransmittableThreadLocal;
/**
* 登录上下文(TTL 适配异步/线程池)
*/
public class SecurityContext {
// 当前用户部门ID
public static final TransmittableThreadLocal<Long> CURRENT_DEPT_ID = new TransmittableThreadLocal<>();
// 是否超级管理员(跳过权限)
public static final TransmittableThreadLocal<Boolean> IS_SUPER_ADMIN = new TransmittableThreadLocal<>();
}
步骤3:实现 DataPermissionHandler(核心业务接口)
这是唯一需要开发者编码的地方,定义权限规则、SQL、黑白名单。
接口方法说明
public interface DataPermissionHandler {
// 1. 获取权限规则列表:表名 + 权限字段
List<DataPermissionRule> getDataPermissionRules();
// 2. 为当前表生成权限 SQL 片段(核心)
String getSqlSegment(Table table, DataPermissionRule rule);
// 3. 忽略指定 Mapper 方法(方法级白名单)
default boolean ignoreMapper(String mappedStatementId) { return false; }
// 4. 忽略指定表(表级白名单)
default boolean ignoreTable(String tableName) { return false; }
}
完整实现类
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionRule;
import net.sf.jsqlparser.schema.Table;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class CustomDataPermissionHandler implements DataPermissionHandler {
// 表级白名单:字典表,永远跳过权限
private static final String WHITE_TABLE = "sys_dict";
/**
* 定义全局权限规则:表名 对应 权限字段
*/
@Override
public List<DataPermissionRule> getDataPermissionRules() {
List<DataPermissionRule> rules = new ArrayList<>();
// sys_user 表 → dept_id
rules.add(DataPermissionRule.create("sys_user", "dept_id"));
// sys_order 表 → dept_id
rules.add(DataPermissionRule.create("sys_order", "dept_id"));
return rules;
}
/**
* 动态生成权限 SQL 片段
*/
@Override
public String getSqlSegment(Table table, DataPermissionRule rule) {
// 超级管理员 → 不拼接权限条件
if (Boolean.TRUE.equals(SecurityContext.IS_SUPER_ADMIN.get())) {
return null;
}
Long deptId = SecurityContext.CURRENT_DEPT_ID.get();
if (deptId == null) {
return null;
}
// 获取表别名,避免多表字段冲突
String tableAlias = table.getAlias() != null ? table.getAlias().getName() : table.getName();
// 拼接条件:别名.字段 = 部门ID
return tableAlias + "." + rule.getColumn() + " = " + deptId;
}
/**
* 表白名单:匹配到白名单表直接跳过权限
*/
@Override
public boolean ignoreTable(String tableName) {
return WHITE_TABLE.equalsIgnoreCase(tableName);
}
/**
* Mapper 方法白名单:指定接口跳过权限
* mappedStatementId = 完整 Mapper 类名.方法名
*/
@Override
public boolean ignoreMapper(String mappedStatementId) {
// 例如:com.example.mapper.UserMapper.getAll
return mappedStatementId.endsWith("UserMapper.getAll");
}
}
步骤4:配置 MP 插件(注册数据权限拦截器)
在 MP 插件配置类中,把 DataPermissionInterceptor 加入插件链。
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
@Configuration
public class MybatisPlusConfig {
// 注入自定义权限处理器
@Resource
private CustomDataPermissionHandler dataPermissionHandler;
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 1. 数据权限插件(建议放在分页之前,保证分页总数也走权限过滤)
DataPermissionInterceptor dataPermissionInterceptor = new DataPermissionInterceptor();
dataPermissionInterceptor.setDataPermissionHandler(dataPermissionHandler);
interceptor.addInnerInterceptor(dataPermissionInterceptor);
// 2. 分页插件(常用搭配)
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
重要顺序:数据权限插件 必须在 分页插件之前,否则分页
count总数不会被权限过滤。
步骤5:业务测试
1)实体 & Mapper
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("sys_user")
public class SysUser {
private Long id;
private String username;
private Long deptId;
}
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {
List<SysUser> getAll(); // 白名单方法
}
2)测试代码
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
public class PermissionTest {
@Autowired
private SysUserMapper userMapper;
@Test
void testDataPermission() {
// 模拟登录用户:部门ID=1001,非超级管理员
SecurityContext.CURRENT_DEPT_ID.set(1001L);
SecurityContext.IS_SUPER_ADMIN.set(false);
// 1. 正常查询:自动追加 dept_id = 1001
List<SysUser> list = userMapper.selectList(null);
list.forEach(System.out::println);
// 2. 白名单方法:不追加权限条件
List<SysUser> all = userMapper.getAll();
all.forEach(System.out::println);
// 清空上下文
SecurityContext.CURRENT_DEPT_ID.remove();
SecurityContext.IS_SUPER_ADMIN.remove();
}
}
3)执行效果(打印最终 SQL)
开启 MP SQL 日志后可见:
selectList(null)最终 SQL:
SELECT id,username,dept_id FROM sys_user WHERE sys_user.dept_id = 1001
getAll()白名单方法:无权限条件sys_dict表查询:表白名单,无权限条件
五、常用权限规则扩展(业务场景)
1. 本人数据权限(create_user_id)
仅查询当前登录人创建的数据:
@Override
public String getSqlSegment(Table table, DataPermissionRule rule) {
Long userId = SecurityContext.CURRENT_USER_ID.get();
if (userId == null) return null;
String alias = table.getAlias() != null ? table.getAlias().getName() : table.getName();
return alias + ".create_user_id = " + userId;
}
2. 部门及下级部门(子查询)
复杂范围直接返回子查询 SQL:
return alias + ".dept_id IN (SELECT id FROM sys_dept WHERE parent_id = 1001)";
3. 超级管理员全开
在 getSqlSegment 中判断管理员角色,返回 null 表示不追加权限条件。
4. 多字段组合权限
可在 getDataPermissionRules 配置多张表、多个字段。
六、与租户插件共存
MP 租户插件 TenantInnerInterceptor + 数据权限插件 可同时使用:
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 1. 数据权限
interceptor.addInnerInterceptor(dataPermissionInterceptor);
// 2. 租户
interceptor.addInnerInterceptor(tenantInnerInterceptor);
// 3. 分页
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
执行顺序:先权限 → 再租户 → 再分页,SQL 会依次拼接两类条件。
七、核心源码执行链路(精简)
MybatisPlusInterceptor#intercept触发拦截- 遍历所有
InnerInterceptor,优先执行DataPermissionInterceptor DataPermissionInterceptor#beforePrepare:- 获取原始 SQL、解析 SQL 类型
- 调用
ignoreMapper判断方法白名单
- JSqlParser 解析 SQL 为 AST
- 遍历 AST 中所有
Table节点,调用ignoreTable判断表白名单 - 匹配
DataPermissionRule,调用getSqlSegment获取权限条件 - 改写 AST,拼接
AND 权限条件 - 重新生成 SQL,交给 MyBatis 继续执行
八、常见问题与最佳实践(避坑)
1. 分页 count 总数不对
原因:数据权限插件放在分页插件之后。
解决:权限插件 → 分页插件。
2. 多表联查字段 ambiguous 冲突
原因:未使用表别名拼接字段。
解决:代码中必须获取 table.getAlias() 拼接条件。
3. 部分 SQL 没被拦截
- 检查是否命中
ignoreTable/ignoreMapper白名单 - 检查 SQL 类型:仅 SELECT/UPDATE/DELETE 生效
4. 上下文为空导致权限条件丢失
解决:登录上下文使用 TransmittableThreadLocal,适配异步、线程池场景。
5. 动态 SQL/XML 复杂语句拦截异常
极少数极端 SQL 导致 AST 解析失败:
- 将该 Mapper 方法加入
ignoreMapper白名单 - 手动在 XML 中处理权限
6. 更新/删除越权
插件默认拦截 UPDATE/DELETE,防止越权修改他人数据;
如需放开编辑接口,将编辑 Mapper 加入方法白名单。
7. 性能优化
- 权限字段
dept_id/user_id建立索引 - 避免权限条件使用大子查询、多表嵌套
九、总结
1. 核心原理
MP 数据权限插件 = MyBatis 拦截器 + JSqlParser AST 语法树解析 + 动态 SQL 改写
在 SQL 执行前自动追加行级权限条件,实现全局无侵入数据隔离。
2. 核心流程
- 实现
DataPermissionHandler定义表-权限字段、黑白名单、权限SQL - 把
DataPermissionInterceptor注册到MybatisPlusInterceptor - 维护登录上下文(TTL 适配异步)
- 自动拦截并改写 SQL
本文来自博客园,作者:蓝迷梦,转载请注明原文链接:https://www.cnblogs.com/hewei-blogs/articles/21936276

浙公网安备 33010602011771号