最安全做法是Java层拼通配符或SQL中用CONCAT等函数,禁用${};若用户输入含%、_需转义并配合ESCAPE子句,且须统一转义逻辑与数据库适配。

直接用 #{} 包裹参数 + 在 Java 层拼通配符,是最简单也最安全的起点;但若用户输入本身含 % 或 _,必须额外转义,否则会语义错乱——这不是注入,但结果完全不可控。
MyBatis 中 like 查询为什么不能写成 '%${keyword}%'
因为 ${} 是字符串替换,不走 JDBC 预编译。输入 admin' -- 会变成 WHERE name LIKE '%admin' -- %',注释掉后续条件;输入 50% off 则被当通配语义解析,匹配所有以 50 开头的记录。
-
#{}才触发预编译,数据库把整个值当纯数据,'%和'_'不会被解释为通配符 - 写成
WHERE name LIKE '%#{keyword}%'是语法错误:MyBatis 不允许在#{}外直接拼% - 正确写法是 Java 层拼:
queryWrapper.like("name", "%" + keyword + "%"),或 SQL 里用函数:CONCAT('%', #{keyword}, '%')
如何安全匹配字面量 % 和 _
用户搜 a_b,你得返回真正含下划线的字符串,而不是匹配 axb、ayb —— 这需要转义,且必须配合 ESCAPE 子句。
- 先选一个转义符,推荐
!(业务文本中极少单独出现) - Java 层转义顺序必须固定:
keyword.replace("!", "!!").replace("%", "!%").replace("_", "!_") - SQL 中写成:
WHERE name LIKE '!%' || #{escapedKeyword} || '!%' ESCAPE '!'(PostgreSQL/Oracle)或CONCAT('!%', #{escapedKeyword}, '!%') ESCAPE '!'(MySQL) - 切忌只在 Java 层转义却忘了 SQL 里加
ESCAPE '!',否则转义符无效,还可能报错
不同数据库对 ESCAPE 的处理差异
同一段转义逻辑,在 MySQL、Oracle、SQL Server 上行为可能不一致,尤其涉及反斜杠 时。
- MySQL 默认用
作转义符,但某些 JDBC 驱动或客户端会提前吃掉一层,导致失效;建议统一用! - Oracle 要求反斜杠必须双写:
'\_'表示字面_,但用!可避免歧义 - SQL Server 的
[、]、-、^也需转义,仅靠!替换不够,要补上:.replace("[", "![")、.replace("]", "!]")、.replace("-", "!-")、.replace("^", "!^") - PostgreSQL 的
ILIKE仍按 SQL 规则处理_和%,不是正则,所以同样要 ESCAPE
MyBatis-Plus 拦截器自动转义的坑
有人用 InnerInterceptor 拦截 LIKE 参数自动加 ,但容易忽略两点:一是拦截不到 XML 中硬编码的 CONCAT 调用,二是没同步修改 SQL 的 ESCAPE 声明。
- 拦截器只能处理
#{}绑定的字符串参数,对CONCAT('%', #{x}, '%')中的#{x}有效,但对WHERE name LIKE #{pattern}中的#{pattern}无效(因为 pattern 已含通配符) - 若拦截器往值里加了
!,而 SQL 模板没写ESCAPE '!',数据库就当普通字符处理,该错配还是错配 - 更稳妥的做法是:把转义逻辑收口到一个工具方法,比如
LikeEscapeUtil.escape(keyword, '!'),所有入口都过一遍,不依赖拦截时机
真正的难点不在“怎么转”,而在“什么时候该转”——用户搜商品名,要支持通配语义;搜订单号,就得字面匹配。同一个字段,不同场景语义不同,得靠业务上下文判断,不能一刀切加 ESCAPE。

















