[特殊字符]防止 MyBatis-Plus 中模糊查詢 `%` 查出全表:實現通配符轉義攔截器

目錄標題

    • ?為什么需要轉義 `%` 和 `_`
    • 🧪 使用案例:防止傳入 `%` 導致全表查詢
    • 🎯 支持哪些場景?
    • ? 攔截器實現思路
    • 🧩 核心攔截器代碼實現
    • 🔐 可選忽略某些 SQL 的轉義

?為什么需要轉義 %_

在使用 MyBatis-Plus 進行模糊查詢時,如果用戶傳入的 % 符號不加以限制或轉義,可能導致嚴重的全表掃描問題,甚至帶來數據泄露風險或性能災難。
在 SQL 中:

  • % 表示匹配任意個字符
  • _ 表示匹配單個字符

例如以下 SQL:

SELECT * FROM user WHERE name LIKE '%';

這條語句會查出全表所有數據。如果直接將用戶輸入的 %_ 用于 LIKE 查詢,容易引發模糊查詢誤傷甚至查詢全表。因此,必須對其進行轉義處理


🧪 使用案例:防止傳入 % 導致全表查詢

假設前端傳入了:

{"username": "%"
}

如果你未做轉義,執行的 SQL 就會變為:

SELECT * FROM user WHERE username LIKE '%';

而添加了本攔截器之后,系統會自動將 % 轉義為:

SELECT * FROM user WHERE username LIKE '\%';

從而避免了無意的全表模糊匹配。


🎯 支持哪些場景?

該攔截器兼容以下查詢方式:

查詢方式是否支持
XML SQL 顯式 LIKE 查詢?
Wrapper 條件構造器?
實體類作為參數查詢?
參數為 Map / 多參數?
嵌套實體對象字段?
@Param 注解參數?

? 攔截器實現思路

我們通過實現 Mybatis-PlusInnerInterceptor 接口,實現對 SQL 中 LIKE 查詢參數的攔截和轉義,主要思路如下:

  1. 檢測 SQL 是否包含 LIKE 語句
  2. 獲取 SQL 中參數綁定的值
  3. 判斷參數是否為字符串且包含特殊字符
  4. 自動將 %_ 添加轉義符 \

🧩 核心攔截器代碼實現

  1. 首先聲明一個攔截器:EscapeLikeSqlInterceptor類
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.lang.Nullable;import java.sql.SQLException;
import java.util.*;/*** MyBatis-Plus LIKE 查詢特殊字符轉義攔截器**/
public class EscapeLikeSqlInterceptor implements InnerInterceptor {public static final String DOT = ".";public static final String PLACEHOLDER_REGEX = "\\?";public static final String DOT_REGEX = "\\.";public static final char LIKE_WILDCARD_CHARACTER = '%';public static final String PLACEHOLDER = "?";public static final String WRAPPER_PARAMETER_PROPERTY = "ew.paramNameValuePairs.";private final String LIKE_SQL = " like ";private static final String SQL_SPECIAL_CHARACTER = "_%";private final String IGNORE = "EscapeLikeSqlIgnore";private static final String PARAM_PREFIX = "__frch_";@Overridepublic void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {// 在查詢前檢查參數是否包含忽略標志或需要轉義的LIKE查詢,不建議開啟不轉義字符if (parameter instanceof Map) {Map<?, ?> parameterMap = (Map<?, ?>) parameter;if (parameterMap.containsKey(IGNORE)) {return;}}
//        // 處理參數為實體對象時的情況,不建議開啟不轉義字符
//        if (!(parameter instanceof Map)) {
//            try {
//                Map<String, Object> paramMap = BeanUtil.beanToMap(parameter);
//                if (paramMap.containsKey(IGNORE)) {
//                    Object ignoreValue = paramMap.get(IGNORE);
//                    if (ignoreValue instanceof Boolean && (Boolean) ignoreValue) {
//                        return;
//                    }
//                }
//            } catch (Exception e) {
//                // 忽略轉換失敗,正常繼續
//            }
//        }if (needEscape(boundSql.getSql())) {return;}escapeSql(boundSql, true);}@Overridepublic void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {// 在更新前檢查參數是否包含忽略標志或需要轉義的LIKE查詢,不建議開啟不轉義字符if (parameter instanceof Map) {Map<?, ?> parameterMap = (Map<?, ?>) parameter;if (parameterMap.containsKey(IGNORE)) {return;}}
//        // 處理參數為實體對象時的情況,不建議開啟不轉義字符
//        if (!(parameter instanceof Map)) {
//            try {
//                Map<String, Object> paramMap = BeanUtil.beanToMap(parameter);
//                if (paramMap.containsKey(IGNORE)) {
//                    Object ignoreValue = paramMap.get(IGNORE);
//                    if (ignoreValue instanceof Boolean && (Boolean) ignoreValue) {
//                        return;
//                    }
//                }
//            } catch (Exception e) {
//                // 忽略轉換失敗,正常繼續
//            }
//        }BoundSql boundSql = ms.getBoundSql(parameter);if (needEscape(boundSql.getSql())) {return;}escapeSql(boundSql, false);}private boolean needEscape(String sql) {// 判斷SQL是否需要轉義,即是否包含LIKE關鍵字且包含占位符return !containLike(sql) || !containPlaceholder(sql);}private boolean containLike(String sql) {// 判斷SQL中是否包含LIKE關鍵字return StrUtil.containsIgnoreCase(sql, LIKE_SQL);}private boolean containPlaceholder(String sql) {// 判斷SQL中是否包含占位符return StrUtil.containsIgnoreCase(sql, PLACEHOLDER);}private boolean containWrapper(String property) {// 判斷屬性是否包含Wrapper參數return StrUtil.contains(property, WRAPPER_PARAMETER_PROPERTY);}private boolean cascadeParameter(String property) {// 判斷屬性是否為級聯參數(即包含點號)return StrUtil.contains(property, DOT);}@SuppressWarnings("unchecked")private void escapeSql(BoundSql boundSql, boolean flag) {// 對SQL中的LIKE查詢參數進行轉義處理String[] split = boundSql.getSql().split(PLACEHOLDER_REGEX);Object parameter = boundSql.getParameterObject();Set<String> processedProperty = new HashSet<>();for (int i = 0; i < split.length; i++) {if (StrUtil.lastIndexOfIgnoreCase(split[i], LIKE_SQL) > -1) {if (parameter instanceof Map) {String property = boundSql.getParameterMappings().get(i).getProperty();if (processedProperty.contains(property)) {continue;}Map<Object, Object> parameterMap = (Map<Object, Object>) parameter;if (containWrapper(property)) {handlerWrapperEscape(property, parameterMap);} else {handlerOriginalSqlEscape(boundSql, property, parameterMap);}processedProperty.add(property);} else if (parameter instanceof String) {BeanUtil.setFieldValue(boundSql.getParameterObject(), "value", addSplashes(((String) parameter)).toCharArray());} else if (parameter instanceof Object) {// 如果參數是實體對象,處理其字段handleEntityFields(parameter, boundSql);}}}}private void handleEntityFields(Object parameter, BoundSql boundSql) {// 遍歷實體類的所有字段,對LIKE查詢參數進行轉義if (parameter != null) {Map<String, Object> fieldValues = BeanUtil.beanToMap(parameter);for (Map.Entry<String, Object> entry : fieldValues.entrySet()) {String property = entry.getKey();Object value = entry.getValue();if (value instanceof String) {// 僅對String類型的LIKE查詢參數進行轉義BeanUtil.setProperty(parameter, property, addSplashes((String) value));} else if (value instanceof Map) {// 處理嵌套實體(Map)handleEntityFields(value, boundSql);}}}}private void handlerWrapperEscape(String property, Map<?, ?> parameterObject) {// 處理Wrapper中的LIKE查詢參數轉義String[] keys = property.split(DOT_REGEX);Object ew = parameterObject.get(keys[0]);if (ew instanceof AbstractWrapper) {Map<String, Object> paramNameValuePairs = ((AbstractWrapper<?, ?, ?>) ew).getParamNameValuePairs();Object paramValue = paramNameValuePairs.get(keys[2]);if (paramValue instanceof String && ((String) paramValue).startsWith("%") && ((String) paramValue).endsWith("%")) {paramNameValuePairs.put(keys[2], String.format("%%%s%%", addSplashes((String) paramValue, LIKE_WILDCARD_CHARACTER)));}}}private void handlerOriginalSqlEscape(BoundSql boundSql, String property, Map<Object, Object> parameterObject) {// 處理原始SQL中的LIKE查詢參數轉義if (cascadeParameter(property)) {String[] keys = property.split(DOT_REGEX, 2);Object parameterBean = parameterObject.get(keys[0]);Object parameterValue = BeanUtil.getProperty(parameterBean, keys[1]);if (parameterValue instanceof String) {BeanUtil.setProperty(parameterBean, keys[1], addSplashes((CharSequence) parameterValue));}} else if (property.startsWith(PARAM_PREFIX)) {Object additionalParameter = boundSql.getAdditionalParameter(property);if (additionalParameter instanceof String) {boundSql.setAdditionalParameter(property, addSplashes((CharSequence) additionalParameter));} else if (additionalParameter instanceof Collection) {boundSql.setAdditionalParameter(property, lists(additionalParameter));}} else {parameterObject.computeIfPresent(property, (key, value) -> {if (value instanceof String) {return addSplashes((CharSequence) value);}return value;});}}private List<?> lists(Object value) {// 處理集合類型參數中的LIKE查詢參數轉義List<?> list = (List<?>) value;List<Object> objects = new ArrayList<>();for (Object o : list) {if (o instanceof Collection) {Object lists = lists(o);objects.add(lists);} else if (o instanceof String) {String s = addSplashes(o.toString());objects.add(s);} else {objects.add(o);}}return objects;}private static String addSplashes(CharSequence content) {// 對內容進行轉義處理return getString(content);}@Nullableprivate static String getString(CharSequence content) {// 對內容進行轉義,如果內容為空則直接返回if (StrUtil.isEmpty(content)) {return StrUtil.str(content);}StringBuilder sb = new StringBuilder();for (int i = 0; i < content.length(); i++) {char c = content.charAt(i);if (StrUtil.contains(SQL_SPECIAL_CHARACTER, c)) {sb.append('\\');}sb.append(c);}return sb.toString();}private static String addSplashes(String content) {// 對字符串內容進行轉義處理return getString(content);}private static String addSplashes(CharSequence content, char trimFix) {// 對內容進行轉義處理,并去除首尾的特定字符if (content.charAt(0) == trimFix) {content = content.subSequence(1, content.length());}if (content.charAt(content.length() - 1) == trimFix) {content = content.subSequence(0, content.length() - 1);}return addSplashes(content);}
}

攔截器中調用的 escapeSql 方法會:

  • 自動識別是否為 LIKE 查詢
  • 根據參數是 Map、實體類、Wrapper 構造器等自動處理字段值
  • 替換 %_ 等特殊字符為 \%\_
  1. 然后再集成攔截器到 Mybatis-Plus
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加特殊字符轉義攔截  注:mybatis-plus特殊字符轉義要在分頁攔截之interceptor.addInnerInterceptor(new EscapeLikeSqlInterceptor());// 添加分頁插件interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;
}

🔐 可選忽略某些 SQL 的轉義

若某些 SQL 不希望執行轉義,可以在參數中加上:

Map<String, Object> paramMap = new HashMap<>();
paramMap.put("EscapeLikeSqlIgnore", true);

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/88284.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/88284.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/88284.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

linux grep的一些坑

grep -a "commit" a.log 可以獲取到所有的數據&#xff08;可以看到a.log所有的commit關鍵詞&#xff09; 但cat a.log|grep "commit" 無法全部獲取到&#xff08;只能看到a.log中部分的的commit&#xff09; 細分析和可能原因&#xff1a; 1. 二進制文件…

牛客 AI 面試 Ultra 版升級:開啟招聘新紀元

每到招聘季&#xff0c;HR 們便陷入繁忙與焦慮。海量簡歷篩選耗費大量人力&#xff0c;初步面試耗費數周時間&#xff0c;好不容易安排好面試官與候選人時間&#xff0c;又可能因各種意外狀況打亂節奏。而牛客 AI 面試 Ultra 版恰似一束光&#xff0c;照亮了招聘流程優化的道路…

OSS與NAS混合云存儲架構:非結構化數據統一管理實戰

AI訓練集管理面臨的核心挑戰&#xff1a;數據規模爆炸式增長與訪問模式多樣化的矛盾。ImageNet等典型數據集已達150TB規模&#xff0c;傳統單一存儲方案面臨三重困境&#xff1a; NAS在PB級場景下硬件成本呈指數增長OSS對象存儲無法滿足高頻隨機訪問需求跨存儲數據訪問導致訓練…

72、單元測試-常用測試注解

72、單元測試-常用測試注解 在單元測試中&#xff0c;常用的測試注解可以幫助組織和管理測試代碼&#xff0c;提高測試的可讀性和可維護性。以下是JUnit和TestNG框架中一些常用的測試注解及其功能&#xff1a; #### JUnit注解 1. **Test** - 標記一個方法為測試方法。 - 可以設…

強化學習在大型語言模型訓練中的最新進展:開源項目深度分析報告

強化學習在大型語言模型訓練中的最新進展&#xff1a;開源項目深度分析報告 引言 近年來&#xff0c;人工智能領域見證了大型語言模型(LLM)的迅速崛起&#xff0c;而強化學習作為機器學習的重要分支&#xff0c;在提升LLM推理能力方面展現出巨大潛力。隨著OpenAI發布o1等推理…

微服務:服務治理

簡單描述這個過程&#xff1a; 生產者與消費者之間的供需關系僅憑這兩者很難有效維持&#xff0c;比如某個消費者掛掉了&#xff0c;而生產者并不知道&#xff0c;就會依然給掛掉的消費者提供內容&#xff0c;那么此時的內容明顯就是未獲取到的&#xff0c;從而出現輸出問題。…

關于數據編碼、進制、位運算的詳細講解(從屬GESP三級)

本章內容 數據編碼基礎 進制轉換 位運算基礎 別讓符號位絆住你的步伐&#xff0c;掌握補碼&#xff0c;讓加減法都成為加法。 一、 數據編碼基礎 目標&#xff1a;掌握 原碼 / 反碼 / 補碼 的定義與互轉、常見進制&#xff08;2 / 8 / 10 / 16&#xff09;互化方法&#x…

實戰項目8(11)

任務場景一 按照下圖完成網絡拓撲搭建和配置 任務要求&#xff1a; 1、根據個人模擬器情況&#xff0c;將各交換機的MAC地址標注在拓撲圖中&#xff0c;并將結果進行截圖。 把每個交換機的這個復制粘貼出來【如上圖綠色標記的】 【SW1】配置 dis stp 【SW2】配置 dis stp 【…

儲能系統的離網,并網,構網,跟網 簡介

一、并網&#xff08;Grid-Connected&#xff09; ? 定義&#xff1a;PCS輸出與公共電網并聯運行&#xff0c;電網主導電壓和頻率&#xff0c;PCS按設定的有功/無功功率注入電網。 ? 特點&#xff1a; 電網存在、電壓頻率穩定 PCS僅作為電源“從機”&#xff0c;不能主導電壓…

Solr 初始環境搭建(Windows)

前言 Apache Solr 是一個開源的、基于 Java 的企業級搜索平臺&#xff0c;構建在 Apache Lucene 之上&#xff0c;提供了強大的全文檢索、分布式搜索、索引管理、高亮顯示、分面搜索等功能。它被廣泛應用于電子商務、內容管理系統&#xff08;CMS&#xff09;、數據分析和大規模…

系統架構設計的全方位視角:深入解析4+1視圖模型及其應用實踐

在當今復雜多變的軟件開發環境中&#xff0c;如何全面把握系統架構&#xff0c;滿足不同利益相關者的需求&#xff0c;是每位架構師面臨的重大挑戰。“41”視圖模型作為一種經典的架構描述框架&#xff0c;為解決這一難題提供了系統化的方法論。本文將深入剖析這一模型的理論基…

vue.js 3: markmap using typescript

在項目目錄文件下&#xff0c;通過cmd運行下述指令。 npm create vuelatest cd vue-projectnpm installnpm run format npm run dev或 npm init vuelatest cd vue-prjectnpm run build --打包項目創建項目沒有node_modules npm init -y npm install vue-routernpm install mark…

聚寬量化——股票時間序列函數

import matplotlib.pyplot as plt import pandas as pd from mpl_finance import candlestick2_ochl import mplfinance as mpf from unittest import TestCaseclass TestPandasKline(TestCase):#讀取股票數據&#xff0c;畫出K線圖def testKLineChart(self):file_name "…

(一)代碼隨想錄 - 數組

代碼隨想錄 一. 數組的理論基礎 概念&#xff1a;數組是存放在連續內存空間上的相同類型數據的集合 特點&#xff1a;&#xff08;1&#xff09;數組可以通過下標進行訪問對應的數據并且下標是從0開始的 -> 隨機訪問&#xff1b;&#xff08;2&#xff09;數組內存空間的地…

Netty內存池核心PoolArena源碼解析

PoolArena 是 Netty 內存池化機制的核心組件之一&#xff0c;它負責管理一整塊或多塊內存&#xff08;PoolChunk&#xff09;&#xff0c;并將這些內存分配給應用程序。每個 PoolArena 實例都與一個特定的線程相關聯&#xff08;通過 PoolThreadCache&#xff09;&#xff0c;或…

echarts-for-react 日歷熱力圖渲染導致白屏 踩坑記錄

先說結果&#xff0c;補上了一行tooltip.trigger后能正常渲染了。 報錯情況&#xff1a; 在頁面中添加了一個日歷熱力圖后&#xff0c;一渲染它就白屏&#xff0c;控制臺報錯如下&#xff1a; echarts-for-react版本是當前最新的3.0.2&#xff0c;嘗試debug但沒看懂源碼這里是…

SpringBoot項目啟動時自動加載數據到Redis的完整實現方案,用于存儲字典,定時任務,登錄用戶等

一、基礎配置 ?在pom.xml中添加必要依賴&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency><groupId>com.baomi…

python:使用 OpenAI CLIP 模型進行圖像與文本的語義匹配,并用彩虹色帶可視化 CLIP 模型的相似度矩陣

作者&#xff1a;CSDN _養樂多_ 本文將介紹如何使用 OpenAI 的 CLIP 模型來實現圖像與文本之間的語義匹配。代碼使用 Python 語言&#xff0c;加載多個圖像與類別文本&#xff0c;并通過計算余弦相似度判斷每張圖片最匹配的文本標簽。 結果如下圖所示&#xff0c; 文章目錄 …

微服務鏈路追蹤在生產環境問題定位中的實戰經驗

微服務鏈路追蹤在生產環境問題定位中的實戰經驗 在當今復雜的系統架構中&#xff0c;微服務之間相互調用形成的鏈路往往變得極其復雜。一旦出現問題&#xff0c;僅憑日志和監控信息常常難以迅速定位根因。鏈路追蹤技術因此成為生產環境中不可或缺的工具&#xff0c;能夠幫助我…

正點原子——直流無刷電機-霍爾傳感基本實現流程

直流無刷電機-霍爾傳感實現流程 初始化TIM以及IO 霍爾狀態讀取函數 uint32_t hallsersor(void) {uint32_t state 0;if(HAL_GPIO_ReadPin(HALL1_TIM_CH1_GPIO,HALL_TIM_CH1_PIN)!RESET){state |0x01;}if(HAL_GPIO_ReadPin(HALL1_TIM_CH2_GPIO,HALL_TIM_CH2_PIN)!RESET){stat…