mybatis-plus 之 权限拦截插件 DataPermissionInterceptor

MyBatis-Plus 数据权限插件原理与完整使用详解

MyBatis-Plus 数据权限插件(DataPermissionInterceptor 是基于 MyBatis 拦截器链实现的行级数据隔离组件,底层结合 SQL 解析、AST 抽象语法树改写,自动为 SELECT/UPDATE/DELETE 语句追加权限条件,实现无代码侵入的数据权限控制(部门权限、个人数据、租户隔离、角色数据范围等)。

本文从前置知识、整体架构、核心原理、完整使用、源码流程、高级场景、避坑全维度讲解。


一、前置基础

1. 适用场景

  • 部门ID过滤:只能查看本部门/下级部门数据
  • 创建人ID过滤:只能查看自己创建的数据
  • 多租户 tenantId 隔离(也可配合租户插件)
  • 角色数据范围:全部数据/本部门/本人数据

2. 核心能力

  1. 拦截 MyBatis 执行的 SQL,动态追加 WHERE 权限条件
  2. 支持主表、多表联查、子查询、分页场景
  3. 支持注解、表名、字段维度配置权限规则
  4. 可指定 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调用ExecutorMyBatis 拦截器链 → 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. 整体执行流程

  1. 应用调用 Mapper 方法,进入 MyBatis 执行链
  2. 经过 MybatisPlusInterceptor,流转到 DataPermissionInterceptor
  3. 判断 SQL 类型:仅处理 SELECT / UPDATE / DELETE(INSERT 一般不拦截)
  4. 通过 JSqlParser 将原始 SQL 解析为 AST 抽象语法树
  5. 调用 DataPermissionHandler
    • 判断当前表是否需要拦截、当前 Mapper 是否白名单
    • 获取「表名 → 权限字段/权限SQL」规则
  6. 遍历 AST 中的表节点,自动拼接 AND 权限条件(自动适配表别名,解决多表联查字段冲突)
  7. 将改写后的 AST 重新生成可执行 SQL
  8. 执行改写后 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'
  1. AST 识别出两张表:sys_user(u)sys_dept(d)
  2. 匹配权限规则:sys_user 需要 dept_id 权限
  3. 自动使用表别名拼接条件:AND u.dept_id IN (1001,1002)
  4. 最终 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_usersys_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 日志后可见:

  1. selectList(null) 最终 SQL:
SELECT id,username,dept_id FROM sys_user WHERE sys_user.dept_id = 1001
  1. getAll() 白名单方法:无权限条件
  2. 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 会依次拼接两类条件。


七、核心源码执行链路(精简)

  1. MybatisPlusInterceptor#intercept 触发拦截
  2. 遍历所有 InnerInterceptor,优先执行 DataPermissionInterceptor
  3. DataPermissionInterceptor#beforePrepare
    • 获取原始 SQL、解析 SQL 类型
    • 调用 ignoreMapper 判断方法白名单
  4. JSqlParser 解析 SQL 为 AST
  5. 遍历 AST 中所有 Table 节点,调用 ignoreTable 判断表白名单
  6. 匹配 DataPermissionRule,调用 getSqlSegment 获取权限条件
  7. 改写 AST,拼接 AND 权限条件
  8. 重新生成 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. 核心流程

  1. 实现 DataPermissionHandler 定义表-权限字段、黑白名单、权限SQL
  2. DataPermissionInterceptor 注册到 MybatisPlusInterceptor
  3. 维护登录上下文(TTL 适配异步)
  4. 自动拦截并改写 SQL
posted @ 2026-07-26 21:16  蓝迷梦  阅读(1)  评论(0)    收藏  举报