SpringBoot基于Mybatis攔截器和JSqlParser實(shí)現(xiàn)數(shù)據(jù)隔離
在構(gòu)建多租戶系統(tǒng)或需要數(shù)據(jù)權(quán)限控制的應(yīng)用時(shí),數(shù)據(jù)隔離是一個(gè)關(guān)鍵問(wèn)題,而解決這一問(wèn)題的有效方案之一是在項(xiàng)目的數(shù)據(jù)庫(kù)訪問(wèn)層實(shí)現(xiàn)數(shù)據(jù)過(guò)濾。本文將介紹如何在 Spring Boot 項(xiàng)目中利用Mybatis的強(qiáng)大攔截器機(jī)制結(jié)合JSqlParser ——一個(gè)功能豐富的 SQL 解析器,來(lái)輕松實(shí)現(xiàn)數(shù)據(jù)隔離的目標(biāo)。本文根據(jù)示例展示如何根據(jù)當(dāng)前的運(yùn)行環(huán)境來(lái)實(shí)現(xiàn)數(shù)據(jù)隔離。
工具介紹
Mybatis攔截器
Mybatis 支持在 SQL 執(zhí)行的不同階段攔截并插入自定義邏輯。
本文將通過(guò)攔截 StatementHandler
接口的 prepare
方法修改SQL語(yǔ)句,實(shí)現(xiàn)數(shù)據(jù)隔離的目的。
JSqlParser
JSqlParser 是一個(gè)開(kāi)源的 SQL 語(yǔ)句解析工具,它可以對(duì) SQL 語(yǔ)句進(jìn)行解析、重構(gòu)等各種操作:
- 能夠?qū)?SQL 字符串轉(zhuǎn)換成一個(gè)可操作的抽象語(yǔ)法樹(AST),這使得程序能夠理解和操作 SQL 語(yǔ)句的各個(gè)組成部分。
- 根據(jù)需求對(duì)解析出的AST進(jìn)行修改,比如添加額外的過(guò)濾條件,然后再將AST轉(zhuǎn)換回SQL字符串,實(shí)現(xiàn)需求定制化的SQL語(yǔ)句構(gòu)建。
SELECT語(yǔ)法樹簡(jiǎn)圖:
詳細(xì)步驟
1. 導(dǎo)入依賴
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>
注意: 如果項(xiàng)目選擇了 Mybatis Plus 作為數(shù)據(jù)持久層框架,那么就無(wú)需另外添加 Mybatis 和 JSqlParser 的依賴。Mybatis Plus 自身已經(jīng)包含了這兩項(xiàng)依賴,并且保證了它們之間的兼容性。重復(fù)添加這些依賴可能會(huì)引起版本沖突,從而干擾項(xiàng)目的穩(wěn)定性。
2. 定義一個(gè)攔截器
攔截所有 query 語(yǔ)句并在條件中加入 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 { /** * 從配置文件中環(huán)境變量 */ @Value("${spring.profiles.active}") private String env; @Override public Object intercept(Invocation invocation) throws Throwable { Object target = invocation.getTarget(); //確保只有攔截的目標(biāo)對(duì)象是 StatementHandler 類型時(shí)才執(zhí)行特定邏輯 if (target instanceof StatementHandler) { StatementHandler statementHandler = (StatementHandler) target; // 獲取 BoundSql 對(duì)象,包含原始 SQL 語(yǔ)句 BoundSql boundSql = statementHandler.getBoundSql(); String originalSql = boundSql.getSql(); String newSql = setEnvToStatement(originalSql); // 使用MetaObject對(duì)象將新的SQL語(yǔ)句設(shè)置到BoundSql對(duì)象中 MetaObject metaObject = SystemMetaObject.forObject(boundSql); metaObject.setValue("sql", newSql); } // 執(zhí)行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語(yǔ)句解析異常:"+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 { // 如果是多表關(guān)聯(lián)查詢,在關(guān)聯(lián)查詢中新增每個(gè)表的環(huán)境變量條件 newWhereExpression = multipleTableJoinWhereExpression(selectBody); } // 將新的where設(shè)置到Select中 selectBody.setWhere(newWhereExpression); } else if (selectBody.getFromItem() instanceof SubSelect) { // 如果是子查詢,在子查詢中新增環(huán)境變量條件 // 當(dāng)前方法只能處理單層子查詢,如果有多層級(jí)的子查詢的場(chǎng)景需要通過(guò)遞歸設(shè)置環(huán)境變量 SubSelect subSelect = (SubSelect) selectBody.getFromItem(); PlainSelect subSelectBody = subSelect.getSelectBody(PlainSelect.class); Expression newWhereExpression = setEnvToWhereExpression(subSelectBody.getWhere(), null); subSelectBody.setWhere(newWhereExpression); } // 獲得修改后的語(yǔ)句 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設(shè)置到Update中 update.setWhere(newWhereExpression); return update.toString(); } else if (statement instanceof Delete) { Delete delete = (Delete) statement; Expression newWhereExpression = setEnvToWhereExpression(delete.getWhere(),null); // 將新的where設(shè)置到delete中 delete.setWhere(newWhereExpression); return delete.toString(); } return originalSql; } /** * 將需要隔離的字段加入到SQL的Where語(yǔ)法樹中 * @param whereExpression SQL的Where語(yǔ)法樹 * @param alias 表別名 * @return 新的SQL Where語(yǔ)法樹 */ private Expression setEnvToWhereExpression(Expression whereExpression, String alias) { // 添加SQL語(yǔ)法樹的一個(gè)where分支,并添加環(huán)境變量條件 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; } } /** * 多表關(guān)聯(lián)查詢時(shí),給關(guān)聯(lián)的所有表加入環(huán)境隔離條件 * @param selectBody select語(yǔ)法樹 * @return 新的SQL Where語(yǔ)法樹 */ 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(); // 將每一個(gè)join的 tx.env = ENV 的條件添加到where中 newWhereExpression = setEnvToWhereExpression(newWhereExpression, joinTableAlias); } } return newWhereExpression; } /** * 新增數(shù)據(jù)時(shí),插入env字段 * @param insert Insert 語(yǔ)法樹 */ private void setEnvToInsert(Insert insert) { // 添加env列 List<Column> columns = insert.getColumns(); columns.add(new Column("env")); // values中添加環(huán)境變量值 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. 測(cè)試
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>
剛進(jìn)入攔截器時(shí),Mybatis 解析的 SQL 語(yǔ)句:
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=?
執(zhí)行完 setEnvToStatement(originalSql)
方法后,得到的新 SQL 語(yǔ)句:
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
剛進(jìn)入攔截器時(shí),Mybatis 解析的 SQL 語(yǔ)句:
INSERT INTO admin ( id, username, code, org_id ) VALUES ( ?, ?, ?, ? )
執(zhí)行完 setEnvToInsert(insert)
方法后,得到的新 SQL 語(yǔ)句:
INSERT INTO admin (id, username, code, org_id, env) VALUES (?, ?, ?, ?, 'test')
Update
剛進(jìn)入攔截器時(shí),Mybatis 解析的 SQL 語(yǔ)句:
UPDATE admin SET username=?, code=?, org_id=? WHERE id=?
執(zhí)行完 setWhere(newWhereExpression)
方法后,得到的新 SQL 語(yǔ)句:
UPDATE admin SET username = ?, code = ?, org_id = ? WHERE id = ? AND env = 'test'
Delete
剛進(jìn)入攔截器時(shí),Mybatis 解析的 SQL 語(yǔ)句:
DELETE FROM admin WHERE id=?
執(zhí)行完 setWhere(newWhereExpression)
方法后,得到的新 SQL 語(yǔ)句:
DELETE FROM admin WHERE id = ? AND env = 'test'
4. 為什么要攔截 StatementHandler 接口的 prepare 方法?
可以注意到,在這個(gè)例子中定義攔截器時(shí) @Signature
注解中攔截的是 StatementHandler
接口的 prepare
方法,為什么攔截的是 prepare
方法而不是 query
和 update
方法?為什么攔截 query
和 update
方法修改 SQL 語(yǔ)句后仍然執(zhí)行的是原 SQL ?
這是因?yàn)?SQL 語(yǔ)句是在 prepare
方法中被構(gòu)建和參數(shù)化的。prepare
方法是負(fù)責(zé)準(zhǔn)備 PreparedStatement
對(duì)象的,這個(gè)對(duì)象表示即將要執(zhí)行的 SQL 語(yǔ)句。在 prepare
方法中可以對(duì) SQL 語(yǔ)句進(jìn)行修改,而這些修改將會(huì)影響最終執(zhí)行的 SQL 。
而 query
和 update
方法是在 prepare
方法之后被調(diào)用的。它們主要的作用是執(zhí)行已經(jīng)準(zhǔn)備好的 PreparedStatement
對(duì)象。在這個(gè)階段,SQL 語(yǔ)句已經(jīng)被創(chuàng)建并綁定了參數(shù)值,所以攔截這兩個(gè)方法并不能改變已經(jīng)準(zhǔn)備好的 SQL 語(yǔ)句。
簡(jiǎn)單來(lái)說(shuō),如果想要修改SQL語(yǔ)句的內(nèi)容(比如增加 WHERE 子句、改變排序規(guī)則等),那么需要在 SQL 語(yǔ)句被準(zhǔn)備之前進(jìn)行攔截,即在 prepare
方法的執(zhí)行過(guò)程中進(jìn)行。
以下是 MyBatis 執(zhí)行過(guò)程中的幾個(gè)關(guān)鍵步驟:
- 解析配置和映射文件: MyBatis 啟動(dòng)時(shí),首先加載配置文件和映射文件,解析里面的 SQL 語(yǔ)句。
- 生成
StatementHandler
和BoundSql
: 當(dāng)執(zhí)行一個(gè)操作,比如查詢或更新時(shí),MyBatis 會(huì)創(chuàng)建一個(gè)StatementHandler
對(duì)象,并包裝了BoundSql
對(duì)象,后者包含了即將要執(zhí)行的 SQL 語(yǔ)句及其參數(shù)。 - 執(zhí)行
prepare
方法:StatementHandler
的prepare
方法被調(diào)用,完成PreparedStatement
的創(chuàng)建和參數(shù)設(shè)置。 - 執(zhí)行
query
或update
: 根據(jù)執(zhí)行的是查詢操作還是更新操作,MyBatis 再調(diào)用query
或update
方法來(lái)實(shí)際執(zhí)行 SQL 。 - 通過(guò)在
prepare
方法進(jìn)行攔截,我們可以在 SQL 語(yǔ)句被最終確定之前更改它,從而使修改生效。如果在query
或update
方法中進(jìn)行攔截,則無(wú)法更改 SQL 語(yǔ)句,只能在執(zhí)行前后進(jìn)行其他操作,比如日志記錄或者結(jié)果處理。
以上就是SpringBoot基于Mybatis攔截器和JSqlParser實(shí)現(xiàn)數(shù)據(jù)隔離的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot Mybatis JSqlParser數(shù)據(jù)隔離的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
mybatis xml如何使用not in 某個(gè)集合的格式
這篇文章主要介紹了mybatis xml如何使用not in 某個(gè)集合的格式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01Java?Process中waitFor()的問(wèn)題詳解
這篇文章主要給大家介紹了關(guān)于Java?Process中waitFor()問(wèn)題的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-12-12SpringSecurity oAuth2.0的四種模式(小結(jié))
本文主要介紹了SpringSecurity oAuth2.0的四種模式,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02eclipse導(dǎo)入IntelliJ IDEA的maven項(xiàng)目的示例
本篇文章主要介紹了eclipse導(dǎo)入IntelliJ IDEA的maven項(xiàng)目的示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-12-12解決MyBatis @param注解參數(shù)類型錯(cuò)誤異常的問(wèn)題
這篇文章主要介紹了解決MyBatis @param注解參數(shù)類型錯(cuò)誤異常的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-02-02Java使用jni清屏功能的實(shí)現(xiàn)(只針對(duì)cmd)
JNI是Java Native Interface的縮寫,它提供了若干的API實(shí)現(xiàn)了Java和其他語(yǔ)言的通信(主要是C&C++)。這篇文章主要介紹了Java使用jni清屏功能的實(shí)現(xiàn)(只針對(duì)cmd) ,感興趣的朋友跟隨腳本之家小編一起學(xué)習(xí)吧2018-05-05Mybatis Criteria使用and和or進(jìn)行聯(lián)合條件查詢的操作方法
這篇文章主要介紹了Mybatis Criteria的and和or進(jìn)行聯(lián)合條件查詢的方法,本文通過(guò)例子給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-10-10