能用#{}就别碰${}:#{}基于预编译机制自动防注入,${}为纯字符串拼接易致SQL注入;仅表名、排序字段等无法加引号的动态SQL结构才可谨慎使用${},且须配合白名单校验。

能用 #{} 就别碰 ${}——这是 MyBatis 防 SQL 注入最直接、最有效的动作。只要把所有本该传值的地方从 ${} 改成 #{},90% 以上的注入风险就消失了。
为什么 ${} 是高危操作
${} 是纯文本替换,MyBatis 解析时直接把变量内容原样塞进 SQL 字符串里,不经过 JDBC 的 PreparedStatement 流程。数据库收到的已经是“拼好”的完整 SQL,用户输入完全可能变成可执行语句的一部分。
- 输入
admin' OR '1'='1→ 拼出WHERE username = 'admin' OR '1'='1' - 输入
test'; DROP TABLE user; --→ 拼出两条语句,第二条被执行 - 哪怕加了单引号或转义,也拦不住绕过手段(比如用十六进制、双写关键字等)
#{} 是怎么做到安全的
#{} 触发的是 JDBC 的预编译机制:SQL 模板先送数据库编译(如 SELECT * FROM user WHERE name = ?),参数再通过 setString()、setInt() 等方法单独传入。数据库只把它当「值」,绝不当「语法」解析。
- 输入
admin' OR '1'='1→ 实际查的是name字段等于这个完整字符串的记录 - MyBatis 自动处理引号、类型转换、空值(
NULL会传setNull()) - 支持嵌套属性:
#{user.name}、#{list[0].id},仍走预编译
哪些地方必须改,哪些可以保留 ${}
绝大多数场景都该用 #{};${} 只在极少数「SQL 结构本身需动态」且无法用占位符替代时才允许出现,且必须配合白名单校验。
- 必须改:WHERE 条件值、INSERT 的 VALUES、UPDATE 的 SET 字段值、IN 子句中的列表项
- 必须改:模糊查询的 LIKE 内容(正确写法是
WHERE name LIKE CONCAT('%', #{keyword}, '%')或数据库函数适配) - 可保留(但要严控):
ORDER BY ${column}、FROM ${tableName}—— 此时${column}必须来自枚举或配置白名单,不能直连前端参数 - 禁止出现:
WHERE ${condition}、AND ${sqlFragment}这类把整段逻辑交给${}拼接的做法
动态 SQL 中混用 ${} 的典型陷阱
很多人以为用了 <if> 就安全了,其实不然——只要里面用了 ${},整个动态块就失效。
- 错误示例:
<if test="sort != null">ORDER BY ${sort}</if>—— 若sort=user_id; DROP TABLE log;,照样执行 - 安全替代:用 Java 层校验
sort是否在["id", "name", "created_at"]中,再传给#{}或白名单${} - 更稳妥做法:用
<bind>预处理 + 白名单映射,避免任何自由拼接
真正难防的不是不会写 #{},而是不知道哪里偷偷用了 ${}。上线前 grep 所有 ${,逐个确认是否必要、是否受控——这才是落地的关键动作。

















