mybatis-plus的eq/like/in等内置方法默认安全,因其底层使用preparedstatement参数绑定,参数值不参与sql解析;但字段名、排序字段等结构部分必须白名单校验或用lambdaquerywrapper编译期锁定。

MyBatis-Plus 的 eq、like、in 等内置方法本身不导致 SQL 注入——只要你不手动拼接字符串、不滥用 apply、不把字段名/排序字段当参数传。
Wrapper 字段名别用字符串动态传
问题核心不在值,而在字段名是否可控。比如 wrapper.eq("status", input) 看似安全,但若 "status" 来自前端或配置项(如 request.getParameter("field")),攻击者就能传 field=update_time; DROP TABLE user,直接破坏 SQL 结构。
- ✅ 安全写法:
wrapper.eq(User::getStatus, input)—— 字段由编译期确定,反射提取,无法被外部篡改 - ❌ 危险写法:
wrapper.eq(request.getParameter("field"), input)—— 字符串字段名完全失控 - orderByAsc/Desc 同理:禁止
wrapper.orderByAsc(sortParam),必须白名单校验后映射到方法引用,例如"username".equals(sortParam) ? wrapper.orderByAsc(User::getUsername) : throw new IllegalArgumentException()
like 查询别手动拼 %
like 方法本身走参数绑定,安全;但手动拼 "%" + keyword + "%" 会引入两个隐患:一是用户输入含 % 或 _ 时语义错乱,二是若后续用 ${} 或字符串拼接,就等于开门揖盗。
- ✅ 推荐:
wrapper.like("username", keyword)—— 框架自动处理通配符和转义 - ✅ 若需前后模糊且兼容特殊字符:
wrapper.like("username", "%" + keyword.replace("%", "\%").replace("_", "\_") + "%").escape("\") - ❌ 避免:
wrapper.apply("username LIKE '%" + keyword + "%'")—— 直接拼接,keyword是admin%' OR '1'='1就崩了
apply 方法是唯一主动开洞的入口
apply 不管你前面用了多少个 User::getXXX,它只看你塞进去的 SQL 片段干不干净。它是 Wrapper 里唯一能绕过参数绑定机制的地方。
- ✅ 安全用法:
wrapper.apply("status = {0}", statusValue)——{0}触发 PreparedStatement 绑定,等价于? - ❌ 危险用法:
wrapper.apply("status = '" + statusValue + "'")—— 字符串拼接,statusValue是1'; TRUNCATE user; --就执行成功 - ⚠️ 注意:
{0}只对值有效,不能用于字段名、表名、ORDER BY 子句等结构部分。想动态排序?只能白名单 + 方法引用,没有捷径
原生 SQL 必须区分 #{} 和 ${}
在 @Select、@Update 等注解里,#{} → PreparedStatement,${} → 字符串替换。后者等同于裸拼,只要参数不可信,就是注入温床。
- ✅ 安全:
@Select("SELECT * FROM user WHERE username = #{username}") - ❌ 危险:
@Select("SELECT * FROM user WHERE username = '${username}'")或@Select("SELECT * FROM ${tableName}") - ⚠️ 动态表名/列名确实需要
${},但必须搭配白名单校验:if (!ALLOWED_TABLES.contains(tableName)) throw new SecurityException();,再拼接
真正容易被忽略的是:字段名、排序字段、表名这些「SQL 结构」从来不受 PreparedStatement 保护——它们根本不会进 ? 占位流程。防注入不是“用了 Wrapper 就万事大吉”,而是每一步都得问一句:这个字符串,是不是可能来自用户?











