{}能防止SQL注入而${}不能,因#{}走PreparedStatement预编译并安全绑定参数,${}是纯字符串替换、易被注入;${}仅限动态SQL结构(如表名、排序字段),须白名单校验。

为什么#{}能防止SQL注入而${}不能
根本原因在于 MyBatis 对两者的处理时机和方式不同:#{} 触发预编译(PreparedStatement),参数被当作占位符传入,数据库驱动自动做类型转换和转义;${} 是纯字符串替换,MyBatis 在 SQL 解析阶段就直接把变量值拼进 SQL 字符串里,不经过 JDBC 预编译流程。
比如 WHERE name = #{name} 最终生成的是 WHERE name = ?,再由 PreparedStatement.setString(1, "admin' --") 安全绑定;而 WHERE name = '${name}' 会直接变成 WHERE name = 'admin' --',注释掉后续条件,造成注入漏洞。
哪些场景必须用${},又该怎么控风险
${} 唯一合理用途是动态拼接 **SQL 结构部分**,比如表名、列名、ORDER BY 子句、LIMIT 参数(非数值上下文)——这些都不能用 ? 占位。
- 表名动态:用
FROM ${tableName},但必须白名单校验tableName值,例如只允许"user"、"order"等枚举值 - 排序字段:用
ORDER BY ${sortColumn} ${sortOrder},sortColumn必须从固定字段列表中取(如"id","create_time"),sortOrder限制为"ASC"或"DESC" - 避免在
${}中拼接用户输入的任意字符串,尤其是带单引号、分号、注释符的内容
#{id} 和 ${id} 在参数类型为数字时表现一样?
表面上看都“能运行”,但行为完全不同:
立即学习“Java免费学习笔记(深入)”;
-
WHERE id = #{id}→ JDBC 绑定为setLong(1, 123),类型安全,支持 null 处理(setNull) -
WHERE id = ${id}→ 直接替换为WHERE id = 123,如果id是 null,会拼出WHERE id = null,语法错误或逻辑异常 - 若
id是字符串类型(如"123' OR '1'='1"),${id}会直接触发注入,#{id}则作为字符串字面量安全绑定
MyBatis Plus 的 Wrapper 是否也遵循这套规则
是的,QueryWrapper 和 UpdateWrapper 底层仍走 MyBatis 的 SQL 构建逻辑。它默认所有条件方法(如 eq("name", value))都使用 #{} 语义;但当你调用 apply("age > {0}", 18) 或 last("LIMIT 1") 时,就进入了 ${} 类似的行为域——apply 中的 {0} 是模板占位,实际仍可能拼接原始字符串,务必确保传入的参数已过滤或来自可信源。
更稳妥的做法是:优先用 gt("age", 18) 这类类型安全方法;非要用 apply 拼 SQL 片段时,对变量值做正则校验(如 ^\d+$ 匹配纯数字)。
SELECT * FROM ${tableName} WHERE status = #{status} ORDER BY ${sortBy} ${sortOrder}真正危险的不是语法本身,是把本该由数据库驱动处理的参数,交给人肉字符串拼接来承担。只要涉及用户输入,${} 就得过白名单或格式校验这一关——漏掉一次,就可能让整张表裸奔。










