SpringBoot基于Mybatis拦截器和JSqlParser实现数据隔离

 更新时间:2024年04月01日 08:32:52   作者:月初_Seth  
本文将介绍如何在 Spring Boot 项目中利用Mybatis的强大拦截器机制结合JSqlParser,一个功能丰富的 SQL 解析器,来轻松实现数据隔离的目标,本文根据示例展示如何根据当前的运行环境来实现数据隔离,需要的朋友可以参考下

在构建多租户系统或需要数据权限控制的应用时,数据隔离是一个关键问题,而解决这一问题的有效方案之一是在项目的数据库访问层实现数据过滤。本文将介绍如何在 Spring Boot 项目中利用Mybatis的强大拦截器机制结合JSqlParser ——一个功能丰富的 SQL 解析器,来轻松实现数据隔离的目标。本文根据示例展示如何根据当前的运行环境来实现数据隔离。

工具介绍

Mybatis拦截器

Mybatis 支持在 SQL 执行的不同阶段拦截并插入自定义逻辑。

本文将通过拦截 StatementHandler 接口的 prepare方法修改SQL语句,实现数据隔离的目的。

JSqlParser

JSqlParser 是一个开源的 SQL 语句解析工具,它可以对 SQL 语句进行解析、重构等各种操作:

  • 能够将 SQL 字符串转换成一个可操作的抽象语法树(AST),这使得程序能够理解和操作 SQL 语句的各个组成部分。
  • 根据需求对解析出的AST进行修改,比如添加额外的过滤条件,然后再将AST转换回SQL字符串,实现需求定制化的SQL语句构建。

SELECT语法树简图:

详细步骤

1. 导入依赖

Mybatis 依赖:

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.3</version>
</dependency>

JSqlParser 依赖:

<dependency>
    <groupId>com.github.jsqlparser</groupId>
    <artifactId>jsqlparser</artifactId>
    <version>4.6</version>
</dependency>

注意: 如果项目选择了 Mybatis Plus 作为数据持久层框架,那么就无需另外添加 Mybatis 和 JSqlParser 的依赖。Mybatis Plus 自身已经包含了这两项依赖,并且保证了它们之间的兼容性。重复添加这些依赖可能会引起版本冲突,从而干扰项目的稳定性。

2. 定义一个拦截器

拦截所有 query 语句并在条件中加入 env 条件

import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.RowConstructor;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.statement.update.Update;
import net.sf.jsqlparser.statement.values.ValuesStatement;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.List;

@Component
@Intercepts(
        {
                @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
        }
)
public class DataIsolationInterceptor implements Interceptor {
    /**
     * 从配置文件中环境变量
     */
    @Value("${spring.profiles.active}")
    private String env;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        //确保只有拦截的目标对象是 StatementHandler 类型时才执行特定逻辑
        if (target instanceof StatementHandler) {
            StatementHandler statementHandler = (StatementHandler) target;
            // 获取 BoundSql 对象,包含原始 SQL 语句
            BoundSql boundSql = statementHandler.getBoundSql();
            String originalSql = boundSql.getSql();
            String newSql = setEnvToStatement(originalSql);
            // 使用MetaObject对象将新的SQL语句设置到BoundSql对象中
            MetaObject metaObject = SystemMetaObject.forObject(boundSql);
            metaObject.setValue("sql", newSql);
        }
        // 执行SQL
        return invocation.proceed();
    }

    private String setEnvToStatement(String originalSql) {
        net.sf.jsqlparser.statement.Statement statement;
        try {
            statement = CCJSqlParserUtil.parse(originalSql);
        } catch (JSQLParserException e) {
            throw new RuntimeException("EnvironmentVariableInterceptor::SQL语句解析异常:"+originalSql);
        }
        if (statement instanceof Select) {
            Select select = (Select) statement;
            PlainSelect selectBody = select.getSelectBody(PlainSelect.class);
            if (selectBody.getFromItem() instanceof Table) {
                Expression newWhereExpression;
                if (selectBody.getJoins() == null || selectBody.getJoins().isEmpty()) {
                    newWhereExpression = setEnvToWhereExpression(selectBody.getWhere(), null);
                } else {
                    // 如果是多表关联查询,在关联查询中新增每个表的环境变量条件
                    newWhereExpression = multipleTableJoinWhereExpression(selectBody);
                }
                // 将新的where设置到Select中
                selectBody.setWhere(newWhereExpression);
            } else if (selectBody.getFromItem() instanceof SubSelect) {
                // 如果是子查询,在子查询中新增环境变量条件
                // 当前方法只能处理单层子查询,如果有多层级的子查询的场景需要通过递归设置环境变量
                SubSelect subSelect = (SubSelect) selectBody.getFromItem();
                PlainSelect subSelectBody = subSelect.getSelectBody(PlainSelect.class);
                Expression newWhereExpression = setEnvToWhereExpression(subSelectBody.getWhere(), null);
                subSelectBody.setWhere(newWhereExpression);
            }

            // 获得修改后的语句
            return select.toString();
        } else if (statement instanceof Insert) {
            Insert insert = (Insert) statement;
            setEnvToInsert(insert);

            return insert.toString();
        } else if (statement instanceof Update) {
            Update update = (Update) statement;
            Expression newWhereExpression = setEnvToWhereExpression(update.getWhere(),null);
            // 将新的where设置到Update中
            update.setWhere(newWhereExpression);

            return update.toString();
        } else if (statement instanceof Delete) {
            Delete delete = (Delete) statement;
            Expression newWhereExpression = setEnvToWhereExpression(delete.getWhere(),null);
            // 将新的where设置到delete中
            delete.setWhere(newWhereExpression);

            return delete.toString();
        }
        return originalSql;
    }

    /**
     * 将需要隔离的字段加入到SQL的Where语法树中
     * @param whereExpression SQL的Where语法树
     * @param alias 表别名
     * @return 新的SQL Where语法树
     */
    private Expression setEnvToWhereExpression(Expression whereExpression, String alias) {
        // 添加SQL语法树的一个where分支,并添加环境变量条件
        AndExpression andExpression = new AndExpression();
        EqualsTo envEquals = new EqualsTo();
        envEquals.setLeftExpression(new Column(StringUtils.isNotBlank(alias) ? String.format("%s.env", alias) : "env"));
        envEquals.setRightExpression(new StringValue(env));
        if (whereExpression == null){
            return envEquals;
        } else {
            // 将新的where条件加入到原where条件的右分支树
            andExpression.setRightExpression(envEquals);
            andExpression.setLeftExpression(whereExpression);
            return andExpression;
        }
    }

    /**
     * 多表关联查询时,给关联的所有表加入环境隔离条件
     * @param selectBody select语法树
     * @return 新的SQL Where语法树
     */
    private Expression multipleTableJoinWhereExpression(PlainSelect selectBody){
        Table mainTable = selectBody.getFromItem(Table.class);
        String mainTableAlias = mainTable.getAlias().getName();
        // 将 t1.env = ENV 的条件添加到where中
        Expression newWhereExpression = setEnvToWhereExpression(selectBody.getWhere(), mainTableAlias);
        List<Join> joins = selectBody.getJoins();
        for (Join join : joins) {
            FromItem joinRightItem = join.getRightItem();
            if (joinRightItem instanceof Table) {
                Table joinTable = (Table) joinRightItem;
                String joinTableAlias = joinTable.getAlias().getName();
                // 将每一个join的 tx.env = ENV 的条件添加到where中
                newWhereExpression = setEnvToWhereExpression(newWhereExpression, joinTableAlias);
            }
        }
        return newWhereExpression;
    }

    /**
     * 新增数据时,插入env字段
     * @param insert Insert 语法树
     */
    private void setEnvToInsert(Insert insert) {
        // 添加env列
        List<Column> columns = insert.getColumns();
        columns.add(new Column("env"));
        // values中添加环境变量值
        List<SelectBody> selects = insert.getSelect().getSelectBody(SetOperationList.class).getSelects();
        for (SelectBody select : selects) {
            if (select instanceof ValuesStatement){
                ValuesStatement valuesStatement = (ValuesStatement) select;
                ExpressionList expressions = (ExpressionList) valuesStatement.getExpressions();
                List<Expression> values = expressions.getExpressions();
                for (Expression expression : values){
                    if (expression instanceof RowConstructor) {
                        RowConstructor rowConstructor = (RowConstructor) expression;
                        ExpressionList exprList = rowConstructor.getExprList();
                        exprList.addExpressions(new StringValue(env));
                    }
                }
            }
        }
    }
}

3. 测试

Select

Mapper:

<select id="queryAllByOrgLevel" resultType="com.lyx.mybatis.entity.AllInfo">
    SELECT a.username,a.code,o.org_code,o.org_name,o.level
    FROM admin a left join organize o on a.org_id=o.id
    WHERE a.dr=0 and o.level=#{level}
</select>

刚进入拦截器时,Mybatis 解析的 SQL 语句:

SELECT a.username,a.code,o.org_code,o.org_name,o.level
        FROM admin a left join organize o on a.org_id=o.id
        WHERE a.dr=0 and o.level=?

执行完 setEnvToStatement(originalSql) 方法后,得到的新 SQL 语句:

SELECT a.username, a.code, o.org_code, o.org_name, o.level 
FROM admin a LEFT JOIN organize o ON a.org_id = o.id 
WHERE a.dr = 0 AND o.level = ? AND a.env = 'test' AND o.env = 'test'

Insert

刚进入拦截器时,Mybatis 解析的 SQL 语句:

INSERT INTO admin  ( id, username, code,   org_id )  VALUES (  ?, ?, ?,   ?  )

执行完 setEnvToInsert(insert) 方法后,得到的新 SQL 语句:

INSERT INTO admin (id, username, code, org_id, env) VALUES (?, ?, ?, ?, 'test')

Update

刚进入拦截器时,Mybatis 解析的 SQL 语句:

UPDATE admin  SET username=?, code=?,   org_id=?  WHERE id=?

执行完 setWhere(newWhereExpression) 方法后,得到的新 SQL 语句:

UPDATE admin SET username = ?, code = ?, org_id = ? WHERE id = ? AND env = 'test'

Delete

刚进入拦截器时,Mybatis 解析的 SQL 语句:

DELETE FROM admin WHERE id=?

执行完 setWhere(newWhereExpression) 方法后,得到的新 SQL 语句:

DELETE FROM admin WHERE id = ? AND env = 'test'

4. 为什么要拦截 StatementHandler 接口的 prepare 方法?

可以注意到,在这个例子中定义拦截器时 @Signature 注解中拦截的是 StatementHandler 接口的 prepare 方法,为什么拦截的是 prepare 方法而不是 query update 方法?为什么拦截 query update 方法修改 SQL 语句后仍然执行的是原 SQL ?

这是因为 SQL 语句是在 prepare 方法中被构建和参数化的。prepare 方法是负责准备 PreparedStatement 对象的,这个对象表示即将要执行的 SQL 语句。在 prepare 方法中可以对 SQL 语句进行修改,而这些修改将会影响最终执行的 SQL 。

queryupdate 方法是在 prepare 方法之后被调用的。它们主要的作用是执行已经准备好的 PreparedStatement 对象。在这个阶段,SQL 语句已经被创建并绑定了参数值,所以拦截这两个方法并不能改变已经准备好的 SQL 语句。

简单来说,如果想要修改SQL语句的内容(比如增加 WHERE 子句、改变排序规则等),那么需要在 SQL 语句被准备之前进行拦截,即在 prepare 方法的执行过程中进行。

以下是 MyBatis 执行过程中的几个关键步骤:

  • 解析配置和映射文件: MyBatis 启动时,首先加载配置文件和映射文件,解析里面的 SQL 语句。
  • 生成 StatementHandlerBoundSql : 当执行一个操作,比如查询或更新时,MyBatis 会创建一个 StatementHandler 对象,并包装了 BoundSql 对象,后者包含了即将要执行的 SQL 语句及其参数。
  • 执行 prepare 方法: StatementHandlerprepare 方法被调用,完成 PreparedStatement 的创建和参数设置。
  • 执行 queryupdate : 根据执行的是查询操作还是更新操作,MyBatis 再调用 queryupdate 方法来实际执行 SQL 。
  • 通过在 prepare 方法进行拦截,我们可以在 SQL 语句被最终确定之前更改它,从而使修改生效。如果在 queryupdate 方法中进行拦截,则无法更改 SQL 语句,只能在执行前后进行其他操作,比如日志记录或者结果处理。

以上就是SpringBoot基于Mybatis拦截器和JSqlParser实现数据隔离的详细内容,更多关于SpringBoot Mybatis JSqlParser数据隔离的资料请关注脚本之家其它相关文章!

相关文章

  • WxJava微信公众号开发入门实战

    WxJava微信公众号开发入门实战

    本文主要介绍了WxJava微信公众号开发入门实战,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-06-06
  • java.imageIo给图片添加水印的实现代码

    java.imageIo给图片添加水印的实现代码

    最近项目在做一个商城项目, 项目上的图片要添加水印①,添加图片水印;②:添加文字水印;一下提供下个方法,希望大家可以用得着
    2013-07-07
  • Java 实现二叉搜索树的查找、插入、删除、遍历

    Java 实现二叉搜索树的查找、插入、删除、遍历

    本文主要介绍了Java实现二叉搜索树的查找、插入、删除、遍历等内容。具有很好的参考价值,下面跟着小编一起来看下吧
    2017-02-02
  • Java并发编程之详解CyclicBarrier线程同步

    Java并发编程之详解CyclicBarrier线程同步

    在之前的文章中已经为大家介绍了java并发编程的工具:BlockingQueue接口,ArrayBlockingQueue,DelayQueue,LinkedBlockingQueue,PriorityBlockingQueue,SynchronousQueue,BlockingDeque接口,ConcurrentHashMap,CountDownLatch,本文为系列文章第十篇,需要的朋友可以参考下
    2021-06-06
  • 一文秒懂IDEA中每天都在用的Project Structure知识

    一文秒懂IDEA中每天都在用的Project Structure知识

    这篇文章主要介绍了一文秒懂IDEA中每天都在用的Project Structure知识,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-10-10
  • spring boot 图片上传与显示功能实例详解

    spring boot 图片上传与显示功能实例详解

    这篇文章主要介绍了spring boot 图片上传与显示功能实例详解,需要的朋友可以参考下
    2017-04-04
  • CentOS 7.9服务器Java部署环境配置的过程详解

    CentOS 7.9服务器Java部署环境配置的过程详解

    这篇文章主要介绍了CentOS 7.9服务器Java部署环境配置,主要包括ftp服务器搭建过程、jdk安装方法以及mysql安装过程,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-07-07
  • 关于JSqlparser使用攻略(高效的SQL解析工具)

    关于JSqlparser使用攻略(高效的SQL解析工具)

    这篇文章主要介绍了关于JSqlparser使用攻略(高效的SQL解析工具),具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-11-11
  • 一文带你深入认识JAVA中的异常

    一文带你深入认识JAVA中的异常

    Java异常处理成为社区中讨论最多的话题之一,下面这篇文章主要给大家介绍了关于JAVA中异常的相关资料,文中通过代码介绍的非常详细,对大家学习或者使用java具有一定的参考借鉴价值,需要的朋友可以参考下
    2024-06-06
  • Java8中的LocalDateTime你会使用了吗

    Java8中的LocalDateTime你会使用了吗

    LocalDateTime 是 Java 8 中日期时间 API 提供的一个类,在日期和时间的表示上提供了更加丰富和灵活的支持,本文就来讲讲LocalDateTime的一些具体使用方法吧
    2023-05-05

最新评论