{}能防SQL注入而${}不能:因#{}触发JDBC预编译,参数作为独立值绑定;${}是纯字符串拼接,用户输入直接嵌入SQL,易导致注入。

能用 #{} 就别碰 ${}——这是 MyBatis-Plus 防 SQL 注入最直接、最有效的动作。只要把所有本该传值的地方从 ${} 改成 #{},90% 以上的注入风险就消失了。
为什么 #{} 能防注入,而 ${} 不能
#{} 触发的是 JDBC 的预编译机制:SQL 模板先送数据库编译(如 SELECT * FROM user WHERE name = ?),参数再通过 setString()、setInt() 等方法单独传入。数据库只把它当「值」,绝不当「语法」解析。
${} 是纯文本替换:MyBatis 解析时直接把变量内容原样塞进 SQL 字符串里,不经过 PreparedStatement 流程。用户输入完全可能变成可执行语句的一部分。
- 输入
admin' OR '1'='1→#{}查的是字段等于这个完整字符串的记录;${}拼出的是WHERE username = 'admin' OR '1'='1',条件恒真 - 输入
test'; DROP TABLE user; --→${}可能直接执行多条语句;#{}把它当一个带单引号的普通字符串处理
MyBatis-Plus 中哪些地方必须用 #{}
所有用户可控的「值」类参数,都必须走 #{}。MyBatis-Plus 的 Wrapper(如 QueryWrapper)底层就是基于 #{} 实现的,所以优先用它。
-
QueryWrapper<user>().like("name", keyword)</user>→ 底层生成name LIKE #{param},安全 - 自定义 XML 中写模糊查询:
WHERE name LIKE CONCAT('%', #{keyword}, '%')(MySQL)或WHERE name LIKE '%' || #{keyword} || '%'(PostgreSQL) - 用
@Select注解时:@Select("SELECT * FROM user WHERE status = #{status}"),禁止写成${status} - IN 查询:用
in("id", ids)(ids是List<long></long>),Wrapper 自动展开为id IN (?, ?, ?);手写 XML 时用<foreach></foreach>+#{item},不是${item}
哪些场景看似要 ${},其实有更安全的替代方案
表名、排序字段、GROUP BY 字段等「SQL 结构」无法用 ? 占位,常被误认为必须用 ${}。但直接拼接仍是高危操作,应改用白名单校验 + 枚举映射。
- ORDER BY 动态字段:
ORDER BY ${sortField}不安全;应先在 Java 层判断sortField是否属于["id", "name", "created_at"]白名单,再传给#{}或严格控制的${} - 动态表名:
FROM ${tableName}风险极高;若业务真需分表(如按月日志表),应在 Service 层根据规则拼接表名,并限定范围(如正则匹配log_202[5-9]_[0-1][0-9]),而非直传前端参数 - LIKE 模糊查询中拼
%:WHERE name LIKE '%${keyword}%'错误;正确做法是用<bind></bind>或数据库函数包裹:<bind name="pattern" value="'%' + keyword + '%'"></bind> ... LIKE #{pattern}
真正难防的不是不会写 #{},而是不知道哪里偷偷用了 ${}——尤其在老项目 XML 中混在 <if></if> 里、或被封装在通用 DAO 工具类里。建议用 IDE 全局搜索 \$\{(注意转义),逐个确认是否白名单可控;对不可控的 ${},一律重构掉。

















