MyBatis-Plus 拦截器实现行级数据权限:JSqlParser 注入原理与 4 种数据范围方案

作者:忆笙智云官方 | 发布时间:2026-05-02 09:30 | 更新时间:2026-06-02 09:30

MyBatis-Plus拦截器实现数据权限原理

引言

在企业级应用中,数据权限控制是比功能权限更细粒度的安全需求。功能权限控制"能不能做",数据权限控制"能看到什么数据"。例如:销售经理只能查看本部门的客户数据,区域总监只能查看所属区域的数据。

MyBatis-Plus提供了强大的拦截器机制,允许在SQL执行前动态注入条件,从而实现透明的行级数据权限控制。本文将深入分析这一机制的实现原理。

一、数据权限的核心问题

1.1 什么是行级数据权限

行级数据权限是指在查询数据时,根据当前用户的权限范围,自动在SQL中添加过滤条件,限制返回的数据行。

-- 原始查询
SELECT * FROM customer WHERE status = 1

-- 注入数据权限后(当前用户只能看本部门数据)
SELECT * FROM customer WHERE status = 1 AND dept_id IN (1, 2, 3)

1.2 实现方案对比

graph TB
    A[数据权限实现方案] --> B[代码层过滤]
    A --> C[视图层过滤]
    A --> D[SQL拦截注入]

    B --> B1[每处查询手动加条件<br/>❌ 遗漏风险高]
    C --> C2[创建数据库视图<br/>❌ 维护成本高]
    D --> D3[拦截器自动注入<br/>✅ 透明无侵入]

    style D3 fill:#d5f9d5
    style B1 fill:#f9d5d5
    style C2 fill:#f9d5d5

二、MyBatis-Plus拦截器机制

2.1 拦截器执行流程

MyBatis-Plus基于MyBatis的Interceptor机制,在SQL执行前对MappedStatement进行拦截和改写。

sequenceDiagram
    participant App as 应用代码
    participant MP as MyBatis-Plus
    participant Interceptor as 数据权限拦截器
    participant JSqlParser as SQL解析器
    participant DB as 数据库

    App->>MP: 执行查询
    MP->>Interceptor: intercept()拦截
    Interceptor->>JSqlParser: 解析原始SQL
    JSqlParser-->>Interceptor: 返回AST语法树
    Interceptor->>Interceptor: 获取当前用户权限规则
    Interceptor->>Interceptor: 构建权限条件表达式
    Interceptor->>JSqlParser: 注入条件到AST
    JSqlParser-->>Interceptor: 返回改写后的SQL
    Interceptor-->>MP: 继续执行
    MP->>DB: 执行改写后的SQL
    DB-->>App: 返回过滤后的数据

2.2 核心接口与类

MyBatis-Plus的数据权限拦截器核心类为DataPermissionInterceptor,它继承自JsqlParserSupport,利用JSqlParser库对SQL进行解析和改写。

/**
 * 自定义数据权限拦截器
 * 在SQL执行前自动注入数据权限条件
 */
public class CustomDataPermissionInterceptor extends JsqlParserSupport
        implements InnerInterceptor {

    private DataPermissionHandler dataPermissionHandler;

    @Override
    public void beforeQuery(Executor executor, MappedStatement ms,
                           Object parameter, RowBounds rowBounds,
                           ResultHandler resultHandler, BoundSql boundSql) {
        // 如果是系统内部调用,跳过数据权限
        if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
            return;
        }
        // 解析并改写SQL
        PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
        mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
    }

    @Override
    public void processSelect(Select select, int index, String sql,
                              Object obj) {
        SelectBody selectBody = select.getSelectBody();
        if (selectBody instanceof PlainSelect) {
            // 处理普通SELECT语句
            this.processPlainSelect((PlainSelect) selectBody);
        } else if (selectBody instanceof SetOperationList) {
            // 处理UNION等集合操作
            SetOperationList setOpList = (SetOperationList) selectBody;
            List<SelectBody> selectBodies = setOpList.getSelects();
            selectBodies.forEach(sb -> {
                if (sb instanceof PlainSelect) {
                    this.processPlainSelect((PlainSelect) sb);
                }
            });
        }
    }

    /**
     * 处理普通SELECT,注入WHERE条件
     */
    protected void processPlainSelect(PlainSelect plainSelect) {
        Expression sqlSegment = dataPermissionHandler.getSqlSegment(
            plainSelect.getWhere(), plainSelect);
        if (sqlSegment != null) {
            plainSelect.setWhere(sqlSegment);
        }
    }
}

三、SQL条件注入原理

3.1 JSqlParser解析SQL

JSqlParser将SQL语句解析为抽象语法树(AST),每个SQL元素对应一个Java对象:

graph TB
    A["SELECT * FROM user WHERE status = 1"] --> B[Select对象]
    B --> C[SelectBody: PlainSelect]
    C --> D[SelectItems: *]
    C --> E[FromItem: user表]
    C --> F[Where: status = 1]

    F --> G[注入权限条件后]
    G --> H["WHERE status = 1 AND dept_id IN (1,2,3)"]

    style G fill:#d5f9d5
    style H fill:#d5f9d5

3.2 条件注入实现

/**
 * 数据权限处理器
 * 根据当前用户权限生成SQL条件
 */
@Component
public class CustomDataPermissionHandler implements DataPermissionHandler {

    @Autowired
    private SecurityUtils securityUtils;

    @Autowired
    private DataScopeService dataScopeService;

    /**
     * 获取数据权限SQL片段
     * @param where 原始WHERE条件
     * @param mappedStatementId Mapper方法的完整路径
     * @return 注入权限条件后的WHERE表达式
     */
    @Override
    public Expression getSqlSegment(Expression where, String mappedStatementId) {
        // 获取当前用户
        LoginUser currentUser = securityUtils.getCurrentUser();
        if (currentUser == null || currentUser.isAdmin()) {
            return where;  // 管理员不受数据权限限制
        }

        // 获取用户的数据权限规则
        DataScope dataScope = dataScopeService.getDataScope(currentUser.getUserId());

        // 构建权限条件
        Expression scopeExpression = buildScopeExpression(dataScope);
        if (scopeExpression == null) {
            return where;
        }

        // 将权限条件与原始WHERE合并
        if (where == null) {
            return scopeExpression;
        }
        return new AndExpression(where, scopeExpression);
    }

    /**
     * 根据数据范围构建SQL条件
     */
    private Expression buildScopeExpression(DataScope scope) {
        switch (scope.getScopeType()) {
            case ALL:
                // 全部数据,不添加条件
                return null;
            case DEPT_ONLY:
                // 仅本部门
                return buildInExpression("dept_id",
                    Collections.singletonList(scope.getDeptId()));
            case DEPT_AND_SUB:
                // 本部门及子部门
                return buildInExpression("dept_id", scope.getDeptIds());
            case CUSTOM:
                // 自定义数据范围
                return buildCustomExpression(scope.getCustomRules());
            default:
                return null;
        }
    }

    /**
     * 构建IN条件表达式
     * 生成: column_name IN (value1, value2, ...)
     */
    private Expression buildInExpression(String columnName, List<Long> values) {
        Column column = new Column(columnName);
        // 使用JSqlParser构建IN表达式
        ItemsList itemsList = new ExpressionList(
            values.stream()
                .map(v -> new LongValue(v))
                .collect(Collectors.toList())
        );
        return new InExpression(column, itemsList);
    }
}

3.3 多表关联时的条件注入

当SQL涉及多表JOIN时,需要精确定位权限条件应该加在哪个表上:

/**
 * 处理多表关联的数据权限注入
 */
protected void processJoinSelect(PlainSelect plainSelect) {
    FromItem fromItem = plainSelect.getFromItem();
    List<Join> joins = plainSelect.getJoins();

    // 处理主表
    if (fromItem instanceof Table) {
        Table table = (Table) fromItem;
        String tableName = table.getName();
        Expression tableCondition = getTableCondition(tableName);
        if (tableCondition != null) {
            plainSelect.setWhere(mergeCondition(
                plainSelect.getWhere(), tableCondition));
        }
    }

    // 处理JOIN表
    if (joins != null) {
        for (Join join : joins) {
            FromItem rightItem = join.getRightItem();
            if (rightItem instanceof Table) {
                Table table = (Table) rightItem;
                String tableName = table.getName();
                Expression tableCondition = getTableCondition(tableName);
                if (tableCondition != null) {
                    // JOIN表的权限条件添加到ON子句
                    join.setOnExpression(mergeCondition(
                        join.getOnExpressions().iterator().next(),
                        tableCondition));
                }
            }
        }
    }
}

四、拦截器注册与配置

4.1 注册拦截器到MyBatis-Plus

/**
 * MyBatis-Plus配置类
 * 注册数据权限拦截器
 */
@Configuration
public class MybatisPlusConfig {

    @Autowired
    private CustomDataPermissionHandler dataPermissionHandler;

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        // 数据权限拦截器(需放在分页拦截器之前)
        CustomDataPermissionInterceptor dataPermissionInterceptor =
            new CustomDataPermissionInterceptor();
        dataPermissionInterceptor.setDataPermissionHandler(dataPermissionHandler);
        interceptor.addInnerInterceptor(dataPermissionInterceptor);

        // 分页拦截器
        interceptor.addInnerInterceptor(
            new PaginationInnerInterceptor(DbType.MYSQL));

        return interceptor;
    }
}

4.2 忽略数据权限的场景

某些场景需要绕过数据权限(如系统内部调用、管理员操作),可通过注解实现:

/**
 * 数据权限忽略注解
 * 标注在Mapper方法上,表示该方法不受数据权限控制
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataPermissionIgnore {
}

// 在拦截器中检查注解
@Override
public void beforeQuery(Executor executor, MappedStatement ms, ...) {
    // 检查Mapper方法是否标注了忽略注解
    String mapperId = ms.getId();
    if (isDataPermissionIgnored(mapperId)) {
        return;
    }
    // ... 正常数据权限处理
}

五、完整执行流程

flowchart TB
    A[Mapper方法调用] --> B{是否有忽略注解?}
    B -->|是| C[直接执行原始SQL]
    B -->|否| D[拦截器拦截]
    D --> E[JSqlParser解析SQL]
    E --> F{获取当前用户}
    F -->|未登录| G[抛出未认证异常]
    F -->|管理员| C
    F -->|普通用户| H[查询用户数据权限规则]
    H --> I{权限范围类型}
    I -->|全部数据| C
    I -->|本部门| J[注入 dept_id = 当前部门ID]
    I -->|本部门及子部门| K[注入 dept_id IN 部门ID列表]
    I -->|自定义| L[注入自定义SQL条件]
    J --> M[合并条件到原始SQL]
    K --> M
    L --> M
    M --> N[执行改写后的SQL]
    N --> O[返回过滤后的数据]

    style C fill:#d5f9d5
    style G fill:#f9d5d5
    style N fill:#d5f9d5

结论与建议

核心优势

  1. 透明无侵入:业务代码无需关心数据权限,拦截器自动处理
  2. 集中管控:权限规则统一管理,避免遗漏
  3. 灵活可扩展:支持多种数据范围类型,可自定义规则

实践建议

  1. 拦截器顺序:数据权限拦截器必须放在分页拦截器之前,否则分页查询的COUNT语句不会被注入权限条件
  2. 性能优化:权限规则应缓存到Redis,避免每次查询都访问数据库获取权限配置
  3. 注解控制:提供忽略注解,让系统内部调用可以绕过数据权限
  4. SQL兼容性:JSqlParser对复杂SQL(如嵌套子查询、存储过程调用)的解析可能存在局限,需充分测试
  5. 日志审计:记录数据权限条件的注入日志,便于排查权限问题

相关资源