高效資料隔離方案:SpringBoot+Mybatis攔截器+JSqlParser全解析

👉 這是一個或許對你有用的社群
🐱 一對一交流/面試小冊/簡歷最佳化/求職解惑,歡迎加入芋道快速開發平臺知識星球。下面是星球提供的部分資料:
👉這是一個或許對你有用的開源專案
國產 Star 破 10w+ 的開源專案,前端包括管理後臺 + 微信小程式,後端支援單體和微服務架構。
功能涵蓋 RBAC 許可權、SaaS 多租戶、資料許可權、商城、支付、工作流、大屏報表、微信公眾號、ERPCRMAI 大模型等等功能:
  • 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 雙版本 

在構建多租戶系統或需要資料許可權控制的應用時,資料隔離是一個關鍵問題,而解決這一問題的有效方案之一是在專案的資料庫訪問層實現資料過濾。
本文將介紹如何在 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

.classmethod

"prepare"

, args = {Connection

.

class

Integer

.

class

})

        }

)

publicclassDataIsolationInterceptorimplementsInterceptor

{

/**

     * 從配置檔案中環境變數

     */


@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) {

thrownew

 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);

            } 

elseif

 (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();

        } 

elseif

 (statement 

instanceof

 Insert) {

            Insert insert = (Insert) statement;

            setEnvToInsert(insert);

return

 insert.toString();

        } 

elseif

 (statement 

instanceof

 Update) {

            Update update = (Update) statement;

            Expression newWhereExpression = setEnvToWhereExpression(update.getWhere(),

null

);

// 將新的where設定到Update中

            update.setWhere(newWhereExpression);

return

 update.toString();

        } 

elseif

 (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 語法樹

     */


privatevoidsetEnvToInsert(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:

<

selectid

=

"queryAllByOrgLevel"

 resultType=

"com.lyx.mybatis.entity.AllInfo"

>

SELECT

 a.username,a.code,o.org_code,o.org_name,o.level

FROMadmin

 a 

leftjoin

 organize o 

on

 a.org_id=o.id

WHERE

 a.dr=

0and

 o.level=

#{level}

</

select

>

剛進入攔截器時,Mybatis 解析的 SQL 語句:
SELECT

 a.username,a.code,o.org_code,o.org_name,o.level

FROMadmin

 a 

leftjoin

 organize o 

on

 a.org_id=o.id

WHERE

 a.dr=

0and

 o.level=?

執行完 setEnvToStatement(originalSql) 方法後,得到的新 SQL 語句:
SELECT

 a.username, a.code, o.org_code, o.org_name, o.level 

FROMadmin

 a 

LEFTJOIN

 organize o 

ON

 a.org_id = o.id 

WHERE

 a.dr = 

0AND

 o.level = ? 

AND

 a.env = 

'test'AND

 o.env = 

'test'

  • Insert
剛進入攔截器時,Mybatis 解析的 SQL 語句:
INSERTINTOadmin

  ( 

id

, username, code,   org_id )  

VALUES

 (  ?, ?, ?,   ?  )

執行完 setEnvToInsert(insert) 方法後,得到的新 SQL 語句:
INSERTINTOadmin

 (

id

, username, code, org_id, env) 

VALUES

 (?, ?, ?, ?, 

'test'

)

  • Update
剛進入攔截器時,Mybatis 解析的 SQL 語句:
UPDATEadminSET

 username=?, code=?,   org_id=?  

WHEREid

=?

執行完 setWhere(newWhereExpression) 方法後,得到的新 SQL 語句:
UPDATEadminSET

 username = ?, code = ?, org_id = ? 

WHEREid

 = ? 

AND

 env = 

'test'

  • Delete
剛進入攔截器時,Mybatis 解析的 SQL 語句:
DELETEFROMadminWHEREid

=?

執行完 setWhere(newWhereExpression) 方法後,得到的新 SQL 語句:
DELETEFROMadminWHEREid

 = ? 

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 語句,只能在執行前後進行其他操作,比如日誌記錄或者結果處理。

歡迎加入我的知識星球,全面提升技術能力。
👉 加入方式,長按”或“掃描”下方二維碼噢
星球的內容包括:專案實戰、面試招聘、原始碼解析、學習路線。
文章有幫助的話,在看,轉發吧。
謝謝支援喲 (*^__^*)

相關文章