Spring Boot + JSqlParser:全面解析数据隔离最佳实践

embedded/2025/2/26 1:59:44/

Spring Boot + JSqlParser:全面解析数据隔离最佳实践

在构建多租户系统或需要进行数据权限控制的应用时,数据隔离是一个至关重要的课题。不同租户之间的数据隔离不仅能够确保数据的安全性,还能提高系统的灵活性和可维护性。随着业务的扩展和需求的变化,单纯依靠传统的分表分库策略往往难以满足日益复杂的业务场景,而更加精细的权限控制和数据隔离机制显得尤为关键。

在这种背景下,Spring Boot结合Mybatis的强大拦截器机制,以及JSqlParser作为SQL解析工具,为我们提供了一个行之有效的解决方案。通过在数据库访问层对SQL进行动态过滤和改造,我们可以在不同的查询、插入、更新、删除操作中灵活地加入租户信息,从而实现多租户数据的有效隔离。本文将深入介绍如何利用这两者的优势,借助拦截器与SQL解析技术,在不修改现有数据结构的基础上,实现对数据的透明隔离。

工具简介

MyBatis 拦截器

MyBatis 提供了丰富的拦截机制,允许在 SQL 执行的各个阶段插入自定义逻辑。本文将通过拦截 StatementHandler 接口的 prepare 方法来修改 SQL 语句,实现数据隔离的目标。

JSqlParser

JSqlParser 是一个开源的 SQL 解析工具,支持 SQL 语句的解析、重构等多种操作。它能够将 SQL 字符串转化为抽象语法树(AST),并允许程序操作和修改 SQL 语句的各个部分。通过对解析后的 AST 进行修改(例如添加环境变量过滤条件),我们可以在 SQL 查询中实现动态的数据隔离。

实现步骤

添加依赖
在 pom.xml 文件中添加 MyBatis 和 JSqlParser 的依赖:

<!-- 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 自带这两个依赖并且确保它们的兼容性。避免重复添加,避免版本冲突。

定义拦截器
我们通过自定义拦截器来修改所有查询 SQL,动态加入基于环境变量的过滤条件。

java">package com.icoderoad.interceptor;import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
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.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.*;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import java.sql.Connection;@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;@Overridepublic Object intercept(Invocation invocation) throws Throwable {Object target = invocation.getTarget();if (target instanceof StatementHandler) {StatementHandler statementHandler = (StatementHandler) target;BoundSql boundSql = statementHandler.getBoundSql();String originalSql = boundSql.getSql();String newSql = applyEnvFilter(originalSql);boundSql.setSql(newSql); // 更新SQL语句}return invocation.proceed(); // 执行SQL}private String applyEnvFilter(String originalSql) {Statement statement;try {statement = CCJSqlParserUtil.parse(originalSql);} catch (JSQLParserException e) {throw new RuntimeException("SQL解析失败: " + originalSql, e);}if (statement instanceof Select) {Select select = (Select) statement;PlainSelect selectBody = (PlainSelect) select.getSelectBody();Expression newWhereExpression = addEnvCondition(selectBody.getWhere());selectBody.setWhere(newWhereExpression);}return statement.toString(); // 返回修改后的SQL语句}private Expression addEnvCondition(Expression whereExpression) {// 生成用于数据隔离的 WHERE 条件AndExpression andExpression = new AndExpression();EqualsTo equalsTo = new EqualsTo();equalsTo.setLeftExpression(new Column("env"));equalsTo.setRightExpression(new StringValue(env));if (whereExpression == null) {return equalsTo;} else {andExpression.setLeftExpression(whereExpression);andExpression.setRightExpression(equalsTo);return andExpression;}}
}

测试查询
假设有以下 SQL 查询:

<select id="queryAllByOrgLevel" resultType="com.icoderoad.entity.AllInfo">SELECT a.username, a.code, o.org_code, o.org_name, o.levelFROM admin aLEFT JOIN organize o ON a.org_id = o.idWHERE a.dr = 0 AND o.level = #{level}
</select>

修改前:
原始 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 = ?

修改后:
经过拦截器处理后:

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、UPDATE 和 DELETE 操作,我们同样可以在 SQL 语句中添加 env 字段:

INSERT
在插入数据时,env 字段会自动添加到 SQL 语句中:

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

更新操作会在 WHERE 子句中添加 env 条件:

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

DELETE
删除操作也会被加上 env 条件:

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

为什么拦截 prepare 方法?
在 MyBatis 中,prepare 方法负责准备 SQL 语句和参数绑定,而 query 和 update 方法主要执行已经准备好的 PreparedStatement。通过拦截 prepare 方法,我们可以确保 SQL 在执行前就已经被修改,从而实现对数据隔离的控制。


http://www.ppmy.cn/embedded/167169.html

相关文章

python和pycharm 和Anaconda的关系

好的&#xff0c;下面我会详细说明 Python、PyCharm 和 Anaconda 三者的关系&#xff0c;并逐一解释它们的功能和作用。 1. Python&#xff08;编程语言&#xff09; 定义&#xff1a;Python 是一种高级编程语言&#xff0c;设计简洁&#xff0c;易于学习&#xff0c;且功能强…

LeetCode 贪心算法经典题目 (C++实现)

121. 买卖股票的最佳时机 题目描述 给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票&#xff0c;并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。 返…

跟着柳叶刀数字健康,学习如何通过病理切片预测分子分类对预后的影响|项目复现

小罗碎碎念 项目复现 今天和大家分享一个非常具有参考价值的项目,手把手带着大家复现一篇发表在柳叶刀数字健康的文章。 花了六个小时才完成的这篇推送,信息量非常大,遇到了很多报错问题,但是解决以后的感觉是非常爽的,先给大家展示一下最终的成果——在同一张切片上,通…

Python的子线程与主线程之间的通信并通知主线程更新UI

新建PLC类 PLC.py import json import time from threading import Threadfrom HslCommunication import SiemensS7Net, SiemensPLCS from PySide6.QtCore import QThread, Signal, QObjectfrom tdm.MsgType import MSG_TYPE_LOG, MSG_TYPE_MSGBOX# 自定义信号类&#xff0c;用…

ubuntu开机自动挂载硬盘

在Ubuntu中实现硬盘自动挂载&#xff0c;可通过以下步骤完成&#xff1a; 1.获取硬盘UUID和文件系统类型 sudo blkid /dev/sdX1 # 替换为你的硬盘分区&#xff08;如sdb1&#xff09;记录输出的UUID和TYPE&#xff08;如ext4/ntfs&#xff09;。 2. 创建挂载目录 sudo mkdi…

Java 常见的面试题(Hibernate)

一、为什么要使用 hibernate&#xff1f; 对JDBC访问数据库的代码做了封装&#xff0c;大大简化了数据访问层繁琐的重复性代码。hibernate是一个基于JDBC的主流持久化框架&#xff0c;是一个优秀的ORM实现。他很大程度的简化DAO层的编码工作hibernate使用Java反射机制&#xf…

【网络安全】从零开始的CTF生活

1、CTF是什么&#xff1f; CTF&#xff08;Capture The Flag&#xff0c;夺旗赛&#xff09;起源于 1996 年 DEFCON 全球大会&#xff0c;是网络安全爱好者之间的竞技游戏。 2、比赛怎么打&#xff1f; 1、解题模式&#xff1a; 与ACM编程竞赛、信息学奥赛类似&#xff0c;…

微服务入门-笔记

微服务入门-笔记 介绍 这个mvn工程里边包含了微服务里最基础的服务拆分、基于nacos的服务注册和发现、配置中心以及OpenFeign版的远程调用。 初始化这个工程是来源于阿里云的脚手架&#xff0c;https://start.aliyun.com/&#xff0c;从这上边创建一个包含各种依赖的工程&am…