惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Check Point Blog
美团技术团队
Microsoft Security Blog
Microsoft Security Blog
大猫的无限游戏
大猫的无限游戏
T
Tailwind CSS Blog
G
Google Developers Blog
博客园 - 聂微东
爱范儿
爱范儿
博客园 - 叶小钗
J
Java Code Geeks
月光博客
月光博客
博客园 - Franky
MongoDB | Blog
MongoDB | Blog
The Cloudflare Blog
宝玉的分享
宝玉的分享
博客园 - 【当耐特】
Microsoft Azure Blog
Microsoft Azure Blog
The GitHub Blog
The GitHub Blog
小众软件
小众软件
WordPress大学
WordPress大学
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Vercel News
Vercel News
Y
Y Combinator Blog
量子位

Liu Zijian's Blog | 一个技术博客

使用Certbot自动续签HTTPS证书 使用Filebeat采集Nginx日志到ES Python的协程 Python中的异常 Python中的类和对象 Python的函数 Python的数据结构,推导式、迭代器和生成器 Spring AI集成多模态模型 LangChain4j多模态 LangChain Tools工具使用 Python中的模块和包 Python全局环境和虚拟环境(venv) LangChain Prompt提示词工程 LangChain4j Tools工具使用 基于Dify搭建AI智能体应用 LangChain4j RAG检索增强生成 Spring AI实现MCP Server Spring AI集成MCP Client LangChain4j Prompt提示词工程 Spring AI使用知识库增强对话功能 Spring AI实现一个智能客服 Spring AI实现一个简单的对话机器人 实现MinIO数据的每日备份 自己实现一个DNS服务 简单理解AI智能体 大模型和大模型应用 LangChain开篇 LangChain4j开篇 一个解析Excel2007的POI工具类 TenantLineInnerInterceptor源码解读
DataPermissionInterceptor源码解读
Liu Zijian · 2025-03-31 · via Liu Zijian's Blog | 一个技术博客

一、概述

DataPermissionInterceptor是MyBatis-Plus中的一个拦截器插件类,位于mybatis-plus-jsqlparser-support模块的com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor,用于实现数据权限功能,它将查询、删除和修改的SQL进行拦截并获得要执行的SQL,并解析出SQL中的表和原有条件,通过一个DataPermissionHandler接口来回调获取每个表的数据权限条件,再和原有的条件拼接在一起形成新的SQL,执行重写后的新SQL,从而实现数据权限功能。因为添加操作无需数据权限控制,因此不处理添加的情况。

本类的实现较为简单,因为对于数据权限来说,对于比较复杂的查询SQL的解析逻辑基本已经由父类完成,具体见:BaseMultiTableInnerInterceptor源码解读,本类作为子类将查询SQL调用父类进行解析重写即可,对于删除和更新的SQL仅仅针对delete和update本身的where条件进行处理,而且是单表操作,因此对于删除和更新来说,只是将表原有条件和数据权限条件做简单的拼接即可。

本文基于MyBatis-Plus的3.5.9版本的源码,并fork了代码: https://github.com/changelzj/mybatis-plus/tree/lzj-3.5.9

public class DataPermissionInterceptor extends BaseMultiTableInnerInterceptor implements InnerInterceptor {

    private DataPermissionHandler dataPermissionHandler;

    @SuppressWarnings("RedundantThrows")
    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {...}

    @Override
    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {...}

    @Override
    protected void processSelect(Select select, int index, String sql, Object obj) {...}

    protected void setWhere(PlainSelect plainSelect, String whereSegment) {...}

    @Override
    protected void processUpdate(Update update, int index, String sql, Object obj) {...}

    @Override
    protected void processDelete(Delete delete, int index, String sql, Object obj) {...}

    protected Expression getUpdateOrDeleteExpression(final Table table, final Expression where, final String whereSegment) {...}

    @Override
    public Expression buildTableExpression(final Table table, final Expression where, final String whereSegment) {...}
}

二、源码解读

2.1 beforeQuery

该方法从InnerInterceptor接口继承而来,是解析查询SQL的起点,MyBatis-Plus执行时就是对实现InnerInterceptor接口的类中的对应方法进行回调的,会传入要执行的SQL并接收重写后的SQL来实现对SQL的修改,在查询SQL执行前进行拦截并调用beforeQuery()beforeQuery()中再去调用parserSingle()

parserSingle()是从父类BaseMultiTableInnerInterceptor自JsqlParserSupport抽象类间接继承而来的,JsqlParserSupport类的功能非常简单,作用是判断SQL是增删改查的哪一种类型,然后分别调用对应的方法开始解析。

当调用parserSingle()并传入SQL时,会在JsqlParserSupport的processParser()方法中先判断是哪一种Statement,然后分别强转为具体的Select、Update、Delete、Insert对象,再调用该类间接继承并重写的processSelect()方法并传入Select对象。

processSelect()方法会再调用父类的processSelectBody()对查询SQL进行解析,对于解析到的每张表和已有条件,再去调用父类的builderExpression()进而再调用buildTableExpression()获取当前表对应的数据权限过滤条件再和已有条件进行拼接。

@SuppressWarnings("RedundantThrows")
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
        return;
    }
    PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
    mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
}

2.2 beforePrepare

该方法和beforeQuery()一样,也是从InnerInterceptor接口中继承而来,因为添加修改和删除SQL都要预编译,因此该方法可作为解析删除和修改SQL的起点,不同的是beforePrepare()调用的是JsqlParserSupport中继承来的parserMulti(),因为查询语句只能一次执行一条,但是增删改语句可以用分号间隔一次执行多条,故需调用parserMulti()将多个语句循环拆开,然后判断并分别强转为具体的Select、Update、Delete、Insert对象,再分别调用该类间接继承并重写的processDelete()processUpdate()方法并分别传入Delete,Update对象,然后直接解析出要删除和更新数据的表和已有删除更新条件,调用父类的andExpression()进而在调用buildTableExpression()来拼接数据权限过滤条件。

@Override
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()));
    }
}

2.3 processSelect

开始一个对查询SQL的解析,当前版本走的是if (dataPermissionHandler instanceof MultiDataPermissionHandler)的新版本的逻辑,先调用processSelectBody()进行解析,对于WITH中的结构,又在调用processSelectBody()后单独组织了一段针对WITH中的查询的解析逻辑。旧版本应该是直接获取where后面的条件直接传递给dataPermissionHandler,在dataPermissionHandler中对where进行追加,而新版本代码是将解析到的表传到dataPermissionHandler,传入的是表名返回表的数据权限条件

@Override
protected void processSelect(Select select, int index, String sql, Object obj) {
    if (dataPermissionHandler == null) {
        return;
    }
    if (dataPermissionHandler instanceof MultiDataPermissionHandler) {
        // 参照 com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor.processSelect 做的修改
        final String whereSegment = (String) obj;
        processSelectBody(select, whereSegment);
        List<WithItem> withItemsList = select.getWithItemsList();
        if (!CollectionUtils.isEmpty(withItemsList)) {
            withItemsList.forEach(withItem -> processSelectBody(withItem, whereSegment));
        }
    } else {
        // 兼容原来的旧版 DataPermissionHandler 场景
        if (select instanceof PlainSelect) {
            this.setWhere((PlainSelect) select, (String) obj);
        } else if (select instanceof SetOperationList) {
            SetOperationList setOperationList = (SetOperationList) select;
            List<Select> selectBodyList = setOperationList.getSelects();
            selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
        }
    }
}

2.4 setWhere

这段代码应该是为旧版本用的,没有走到

/**
 * 设置 where 条件
 *
 * @param plainSelect  查询对象
 * @param whereSegment 查询条件片段
 */
protected void setWhere(PlainSelect plainSelect, String whereSegment) {
    if (dataPermissionHandler == null) {
        return;
    }
    // 兼容旧版的数据权限处理
    final Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), whereSegment);
    if (null != sqlSegment) {
        plainSelect.setWhere(sqlSegment);
    }
}

2.5 processUpdate

/**
 * update 语句处理
 */
@Override
protected void processUpdate(Update update, int index, String sql, Object obj) {
    final Expression sqlSegment = getUpdateOrDeleteExpression(update.getTable(), update.getWhere(), (String) obj);
    if (null != sqlSegment) {
        update.setWhere(sqlSegment);
    }
}

2.6 processDelete

/**
 * delete 语句处理
 */
@Override
protected void processDelete(Delete delete, int index, String sql, Object obj) {
    final Expression sqlSegment = getUpdateOrDeleteExpression(delete.getTable(), delete.getWhere(), (String) obj);
    if (null != sqlSegment) {
        delete.setWhere(sqlSegment);
    }
}

2.7 getUpdateOrDeleteExpression

针对更新和删除的SQL,不同于查询,当更新后的值是子查询或更新删除条件的值是一个子查询的时候,不会为这个子查询中的表追加条件,仅把针对整个update或delete语句的条件本身和要追加的数据权限过滤条件进行AND和OR拼接,因此会直接把表名和WHERE条件调用父类的andExpression(table, where, whereSegment)进行拼接,方法的返回值即为拼接后的结果,直接返回。

protected Expression getUpdateOrDeleteExpression(final Table table, final Expression where, final String whereSegment) {
    if (dataPermissionHandler == null) {
        return null;
    }
    if (dataPermissionHandler instanceof MultiDataPermissionHandler) {
        return andExpression(table, where, whereSegment);
    } else {
        // 兼容旧版的数据权限处理
        return dataPermissionHandler.getSqlSegment(where, whereSegment);
    }
}

2.8 buildTableExpression

传入表名,返回表要追加的数据权限过滤条件,具体哪个表需要怎样的数据权限条件,会通过回调dataPermissionHandler.getSqlSegment()让DataPermissionHandler的实现类根据具体业务来确定

@Override
public Expression buildTableExpression(final Table table, final Expression where, final String whereSegment) {
    if (dataPermissionHandler == null) {
        return null;
    }
    // 只有新版数据权限处理器才会执行到这里
    final MultiDataPermissionHandler handler = (MultiDataPermissionHandler) dataPermissionHandler;
    return handler.getSqlSegment(table, where, whereSegment);
}