[特殊字符]防止 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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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…