高效数据隔离方案:SpringBoot+Mybatis拦截器+JSqlParser全解析

高效数据隔离方案:SpringBoot+JSqlParser 全解析 Mybatis 支持在 SQL 执行的不同阶段拦截并插入自定义逻辑。本文将通过拦截接口的prepare方法修改SQL语句,实现数据隔离的目的。拦截所有 query 语句并在条件中加入 env 条件@Component/*** 从配置文件中环境变量*/@Override//确保只有拦截的目标对象是 StatementHandler 类型时才执行特定逻辑// 获取 BoundSql 对象,包含原始 SQL 语句。 阅读详情

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料: 

e9354fb7711a41b6810abda3237e287b.gif

👉这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号、ERP、CRM、AI 大模型等等功能:

  • Boot 多模块架构:https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • Cloud 微服务架构:https://gitee.com/zhijiantianya/yudao-cloud

  • 视频教程:https://doc.iocoder.cn

【国内首批】支持 JDK 17/21 + SpringBoot 3.3、JDK 8/11 + Spring Boot 2.7 双版本 

来源:juejin.cn/post/
7352075798067986458


在构建多租户系统或需要数据权限控制的应用时,数据隔离是一个关键问题,而解决这一问题的有效方案之一是在项目的数据库访问层实现数据过滤。

本文将介绍如何在 Spring Boot 项目中利用Mybatis的强大拦截器机制结合JSqlParser —— 一个功能丰富的 SQL 解析器,来轻松实现数据隔离的目标。本文根据示例展示如何根据当前的运行环境来实现数据隔离。

工具介绍

Mybatis拦截器

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

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

JSqlParser

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

  • 能够将 SQL 字符串转换成一个可操作的抽象语法树(AST),这使得程序能够理解和操作 SQL 语句的各个组成部分。

  • 根据需求对解析出的AST进行修改,比如添加额外的过滤条件,然后再将AST转换回SQL字符串,实现需求定制化的SQL语句构建。

SELECT语法树简图:

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

详细步骤

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 。

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

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

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

  1. 解析配置和映射文件: MyBatis 启动时,首先加载配置文件和映射文件,解析里面的 SQL 语句。

  2. 生成 StatementHandler 和 BoundSql: 当执行一个操作,比如查询或更新时,MyBatis 会创建一个 StatementHandler 对象,并包装了 BoundSql 对象,后者包含了即将要执行的 SQL 语句及其参数。

  3. 执行 prepare 方法: StatementHandler 的 prepare 方法被调用,完成 PreparedStatement 的创建和参数设置。

  4. 执行 query 或 update: 根据执行的是查询操作还是更新操作,MyBatis 再调用 query 或 update 方法来实际执行 SQL 。

通过在 prepare 方法进行拦截,我们可以在 SQL 语句被最终确定之前更改它,从而使修改生效。如果在 query 或 update 方法中进行拦截,则无法更改 SQL 语句,只能在执行前后进行其他操作,比如日志记录或者结果处理。


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,“长按”或“扫描”下方二维码噢:

099eec8edacc6be49631d54bc5abfef2.png

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

c7ee188f15cc002b1f203fcf98cdfa77.png

6a324affb3d9ec2640c4c1d8ee8336a5.png128e99da18e376a05f5d46352690bb3c.pngc591704c3276ec86e0a4f3b65138e932.png0707dc0b493836776805fc85bc26e17f.png

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
SpringBoot+MyBatis数据权限实战:基于拦截器的SQL动态过滤方案 数据权限是企业级应用安全架构的核心组成部分,它确保不同角色的用户在同一功能界面下只能访问授权范围内的数据。其实现原理是在数据库查询执行时动态注入过滤条件,从而在数据源头实现访问控制。这一技术对于保障系统安全、满足合规要求具有关键价值,广泛应用于金融、医疗、企业管理等对数据敏感度高的行业场景。通过MyBatis拦截器机制,可以在SQL执行前自动拼接权限条件,实现与业务逻辑的完全解耦。本文结合SpringBoot框架,深入探讨如何设计可解析的权限规则引擎,并解决多表关联、分页查询等工程实践中的典型问题,为构建健 阅读详情

相关推荐

PageHelper分页原理与MyBatis拦截器机制解析

数据库分页是Web开发中的基础技术,通过LIMIT/OFFSET或ROWNUM等语法实现数据分批提取。其核心原理是SQL拦截与改写,MyBatis插件机制为此提供了扩展点。PageHelper作为主流分页组件,利用ThreadLocal存储分页参数,通过拦截器自动生成分页SQL,显著提升开发效率。该技术特别适用于SpringBoot+MyBatis技术栈,支持MySQL、Oracle等十余种数据库方言。在实际工程中需注意分页失效场景,如线程切换问题,并可通过关闭count查询优化性能。内存分页与SQL分页的

311

mybatis拦截器,并使用jsqlparser重写sql

mybatis拦截器 mybatis拦截器基础知识 编写类继承mybatis的Interceptor接口,并实现其相关方法。mybatis的拦截器,是被动态代理类主动调用的。 intercept:在拦截时,需要执行的业务逻辑 plugin:是否代理Executor、ParameterHandler、ResultSetHandler、StatementHandler对象中的某个或某些, 如果代理,则返回相应对象的代理对象,否则返回原对象。根据类上Intercepts注解决定是否返回代理对象。 setProp

zhaocuit的博客 4633

Spring Boot+MyBatis拦截器+JSqlParser全解析

Mybatis 支持在 SQL 执行的不同阶段拦截并插入自定义逻辑。本文将通过拦截接口的prepare方法修改SQL语句,实现数据隔离的目的。拦截所有 query 语句并在条件中加入 env 条件@Component/*** 从配置文件中环境变量*/@Override//确保只有拦截的目标对象是 StatementHandler 类型时才执行特定逻辑// 获取 BoundSql 对象,包含原始 SQL 语句。

tangjieqing的博客 878

【Mybatis Plus】JSqlParser解析sql语句

JSqlParser 是一个用于解析 SQL 语句的 Java 库。它可以将 SQL 语句解析为一个 Java 对象树,允许你以编程的方式对 SQL 语句进行分析、修改和操作。它支持多种 SQL 语句类型,包括但不限于 SELECT、INSERT、UPDATE、DELETE、CREATE、ALTER 等。例如,对于 SQL 语句 “SELECT column1, column2 FROM table1 WHERE column1 = ‘value’”,JSqlParser 可以将其解析为一个 Java 对象

weixin_44823875的博客 5005

Springboot 自定义 Mybatis拦截器,实现 动态查询条件SQL自动组装拼接(玩具)

Springboot 封装整活 Mybatis 动态查询条件SQL自动组装拼接

默默不代表沉默 6977

Java分页插件PageHelper的集成与应用

PageHelper是一个流行的MyBatis分页插件,它易于集成且功能强大,为广大开发人员提供了便捷的分页解决方案。这款插件不仅支持多数据库,而且拥有灵活的分页模式,比如物理分页和逻辑分页。PageHelper通过拦截器实现了透明化的分页处理,不需要对SQL语句进行大的修改。

weixin_42579969的博客 805

在Spring boot 中执行 sql 利用 Mybatis 拦截器 获取执行sql语句

解决方案:在开发中 添加 更新 删除的 基础数据 执行的 sql 语句 存到开发数据库 需要同步 到线上时。需求: 有的时候 需要动态 拼接 sql 比如 数据权限 根据权限不同 拼接不同的 where 条件。需求: 开发 生成 数据库 同步基础数据 但是又不能直接覆盖表 这时候同步起来 很玛法。最后再执行 sql 的时候 数据库表里 就会记录 执行的 sql 语句。下面的代码 主要是 拦截 执行的 sql 语句 存储到 数据库。直接执行 存在 开发数据的 sql 语句 即可。

Json的知识梦工厂 4936

jsqlparser-0.9.5

时常会遇到很多情况,我们需要对SQL语句进行替换或者拼接。以往我们可能会用StringBuild来进行拼接。但是这样的操作不仅看起来不优雅,也不简洁。面对复杂的sql,有很多子循环的情况下,并不适用。 因此我们需要借助一些工具来实现这个功能。这里我们使用jsqlparser这个sql解析工具。

Mybatis-plus解析sql发生异常:net.sf.jsqlparser.JSQLParserException

在使用mybatis-plus的过程中,有一个jsonb字段使用了jsonb_exists_any (field_name, text[]) Caused by: net.sf.jsqlparser.JSQLParserException: Encountered unexpected token MyBatis_Plus框架中,有多租户的功能,MP会进行数据权限的过滤导致的,可以使用下面的注解(+配置)解决:@SqlParser(filter = true)、@InterceptorIgnore(tena

qq_24615389的博客 6174

别再只用跳层连接了!手把手教你用DASI和MDCR模块提升红外小目标检测精度

本文详细介绍了如何利用DASI和MDCR模块提升红外小目标检测精度。通过分析传统跳层连接的缺陷,提出维度感知的特征选择性融合(DASI)和多尺度感受野精炼器(MDCR)的创新方案,显著提升小目标检测性能。实验证明,该方法在多个场景下AP@0.5提升达15.4%,特别适用于安防监控和遥感图像分析。

weixin_28715081的博客 224

别再乱改宏定义了!STM32F103程序移植保姆级教程(从C8T6到ZET6实战)

本文详细解析了STM32F103程序从C8T6移植到ZET6的实战步骤,重点介绍了硬件差异、开发环境配置、时钟陷阱及外设处理等关键点。通过修改宏定义(如STM32F10X_HD)、调整启动文件和时钟配置,确保移植顺利进行。适合嵌入式开发者参考,避免常见移植问题。

weixin_42567497的博客 217

高效数据隔离方案:Spring Boot+MyBatis拦截器+JSqlParser全解析!

当执行一个操作,比如查询或更新时,MyBatis 会创建一个 StatementHandler 对象,并包装了 BoundSql 对象,后者包含了即将要执行的 SQL 语句及其参数。后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,Spring Cloud Alibaba 等等,方法而不是 query 和 update 方法?根据需求对解析出的AST进行修改,比如添加额外的过滤条件,然后再将AST转换回SQL字符串,实现需求定制化的SQL语句构建。

犬小哈 114

数字IC后端实现之快速获取innovus中drv violation的所有net list

数字IC后端设计实现之快速获取innovus中drv violation的所有net list

weixin_37584728的博客 3350
上一篇: 解析JSON的这 6 种方案,真香!
下一篇: 今年金三银四的实感。。
公众号-芋道源码
博客等级 码龄10年 1634粉丝 377原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值