{}通过PreparedStatement预编译将参数作为纯数据绑定,数据库仅执行固定SQL结构,恶意输入如"1; DROP TABLE user; --"也被当字符串处理;${}直接字符串拼接,输入含' OR '1'='1等会篡改SQL逻辑,仅表名、字段名等语法位置必须用${},且须白名单校验。

因为 #{} 触发 PreparedStatement 预编译,参数永远作为「值」被绑定,数据库不会把它当 SQL 代码解析;而 ${} 是字符串拼接,用户输入会直接混入 SQL 文本,一旦没做白名单校验,' OR '1'='1 这类输入就能改变语句逻辑。
预编译到底怎么拦住注入的?
MyBatis 把 #{id} 替换成 ? 占位符,再调用 PreparedStatement.setLong(1, id) 或 setString(1, value) 安全传参。数据库看到的始终是:
SELECT * FROM user WHERE id = ?
即使你传 "1; DROP TABLE user; --",它也只会被当作一个字符串字面量塞进 ? 位置,最终执行的是:
SELECT * FROM user WHERE id = '1; DROP TABLE user; --'
——这是一条查不到数据的普通语句,不是两条命令。
为什么 ${} 一拼就炸?
${} 不经过 JDBC 预编译流程,MyBatis 直接把变量值原样插进 SQL 字符串里。比如:
-
WHERE name = '${name}'+ 输入"admin' --"→WHERE name = 'admin' --' -
ORDER BY ${column}+ 输入"id ASC; TRUNCATE log; --"→ORDER BY id ASC; TRUNCATE log; --
分号、注释符、关键字全被数据库原样执行。这不是“可能出问题”,是只要参数可控,必然可注入。
哪些地方非用 ${} 不行?
因为 PreparedStatement 禁止动态表名、列名、ORDER BY 子句中的 ASC/DESC,这些语法节点不能加引号,也不能用 ? 占位:
-
SELECT * FROM ${tableName}—— 表名不能带引号,#{tableName}会变成FROM 'user'报错 -
ORDER BY ${column} ${orderType}——ASC是关键字,不是值,#{orderType}会加单引号变成ORDER BY name 'ASC' -
INSERT INTO ${table} VALUES (...)—— MySQL 批量插入时部分场景需动态表
这些地方用 ${} 不是偷懒,是 JDBC 层面的硬性限制。
用 ${} 时最常漏掉的安全动作
很多人知道“要校验”,但实际只做模糊检查或长度限制。真正有效的兜底只有:
- 白名单硬编码:比如排序字段只允许
Set.of("id", "name", "created_at") - 正则强约束:表名必须匹配
^[a-zA-Z][a-zA-Z0-9_]{2,31}$,且查数据库元信息确认存在 - 禁止任何用户直传:前端传字段别名(如
sort=userName),后端映射为真实列名"name"
没走这几步,${column} 和裸写 Statement 没本质区别。生产环境里,${} 出问题从来不是“会不会”,而是“什么时候”。











