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

news/2025/2/26 3:10:15

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;

    @Override
    public 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.level
    FROM admin a
    LEFT JOIN organize o ON a.org_id = o.id
    WHERE 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.niftyadmin.cn/n/5867110.html

相关文章

<tauri><rust><GUI><PLC>基于tauri,编写一个串口485调试助手

前言 本文是基于rust和tauri,由于tauri是前、后端结合的GUI框架,既可以直接生成包含前端代码的文件,也可以在已有的前端项目上集成tauri框架,将前端页面化为桌面GUI。 环境配置 系统:windows 10平台:visual studio code语言:rust、javascript库:tauri2.0概述 本文基…

51单片机-AT24CXX存储器工作原理

1、AT24CXX存储器工作原理 1.1、特点&#xff1a; 与400KHz&#xff0c;I2C总线兼容1.8到6.0伏工作电压范围低功耗CMOS技术写保护功能当WP为高电平时进入写保护状态页写缓冲器自定时擦写周期100万次编程/擦除周期可保存数据100年8脚DIP SOIC或TSSOP封装温度范围商业级和工业级…

Git-速查

Git 安装 Git 之后&#xff0c;你可以… 配置全局用户信息&#xff08;推荐&#xff09; 全局设置&#xff0c;创建本地仓库时默认分支名称为 main&#xff08;你需要什么名称就该什么名称&#xff09;【推荐配置为 main 】 git config --global init.defaultBranch main全…

[Linux]从零开始的STM32MP157 U-Boot网络命令讲解及相关配置

一、前言 在上一次的STM32MP157的教程中&#xff0c;教大家STM32MP157基础命令的使用&#xff0c;同时也验证了我们STM32MP157U-Boot的部分功能。因为U-Boot的网络配置部分比较复杂&#xff0c;所以在上一次的教程中并没有涉及。那么本次教程&#xff0c;我会为大家详细的讲解U…

面试八股文--数据库基础知识总结(1)

1、数据库的定义 数据库&#xff08;DataBase&#xff0c;DB&#xff09;简单来说就是数据的集合数据库管理系统&#xff08;Database Management System&#xff0c;DBMS&#xff09;是一种操纵和管理数据库的大型软件&#xff0c;通常用于建立、使用和维护数据库。数据库系统…

在scss中使用for循环生成头像/icon

在业务中有时会出现相同样式不同icon的情况出现&#xff0c;如果我们使用scss中的for循环的话这个问题很好解决 先列出标签结构 <div v-for"item in function_data" :key"item.id" class"signal_btn"><div :class"btn_avatar (…

Eureka、ZooKeeper 和 Nacos 之间的对比

Eureka、ZooKeeper 和 Nacos 都是分布式系统中常用的服务注册与发现工具&#xff0c;但它们的定位、功能和适用场景有所不同。作为一名开发者&#xff0c;理解它们之间的对比有助于选择合适的技术栈。以下从多个维度进行详细比较&#xff1a; 1. 基本概述 Eureka 来源&#xff…

LeetCode - 23 合并 K 个升序链表

题目来源 23. 合并 K 个升序链表 - 力扣&#xff08;LeetCode&#xff09; 题目描述 给你一个链表数组&#xff0c;每个链表都已经按升序排列。 请你将所有链表合并到一个升序链表中&#xff0c;返回合并后的链表。 示例 示例 1&#xff1a; 输入&#xff1a;lists [[1,4,…