{}防注入的根本原因是其强制使用PreparedStatement预编译机制,使SQL模板与参数分离,数据库天然区分指令与数据;${}则在MyBatis解析阶段直接字符串拼接,将用户输入嵌入SQL语法结构,极易引发注入。

预编译机制是#{}防注入的唯一原因
不是“#{}更安全”,而是它强制走 JDBC 的 PreparedStatement 流程:SQL 模板先发给数据库编译(如 SELECT * FROM user WHERE id = ?),参数值后续用 setInt(1, 5) 或 setString(2, "admin' OR 1=1 --") 单独传入。数据库引擎天然区分「指令」和「数据」,恶意字符串只会被当作文本值处理,不可能触发语法解析。
而 ${} 在 MyBatis 解析阶段就完成字符串拼接,等同于 Java 里写 "SELECT * FROM user WHERE name = '" + userInput + "'",拼完才把整条 SQL 发给数据库——此时攻击载荷已嵌入语法结构中,防不胜防。
关键点:
-
#{}生成的是带?的预编译模板,${}生成的是完整、可执行的 SQL 字符串 - 数据库对
?参数不做任何 SQL 解析,只做类型绑定;对拼接后的字符串则完整走词法/语法分析流程 - 即使
#{}传入"1' OR '1'='1",最终执行的仍是WHERE username = '1'' OR ''1''=''1'(单引号被转义为两个)
哪些场景必须用${}?不是“可以”,是“只能”
当你要替换的部分在 SQL 中属于「语法成分」而非「数据值」时,#{} 会加引号导致语法错误,${} 成了唯一合法选择。但必须满足两个前提:参数来源可信、不可被用户直接控制。
典型场景:
- 动态表名:
SELECT * FROM ${tableName} WHERE id = #{id}—— 若用#{tableName},生成的是FROM 'user_202611',MySQL 报错 - ORDER BY 字段或方向:
ORDER BY ${sortField} ${sortOrder}——#{sortField}会变成ORDER BY 'name',排序失效 - UNION 查询中的子句结构(如
SELECT ... UNION ALL ${subQuery})—— 此类高级用法极少见,且需严格校验subQuery白名单
注意:${} 本身不校验内容,你得自己确保传入的 tableName 是白名单里的 "user_202610",而不是用户提交的 "user; DROP TABLE config; --"。
Like 查询怎么写才不踩坑?别手写%拼接
常见错误写法:WHERE name LIKE '%${name}%' 或 WHERE name LIKE '%' || #{name} || '%'(Oracle 风格)。前者直接开注入口,后者在部分方言里因字符串拼接位置不当仍可能出问题。
正确做法统一用 #{} ,把通配符交给 Java 层处理:
- DAO 方法接收原始关键词,如
findUsersByName("zhang") - 在 service 或 mapper 接口层拼好前缀后缀:
@Param("name") String name + "%" + name + "%" - SQL 中保持干净:
WHERE name LIKE #{name}
如果必须在 XML 里处理(如老项目无法改接口),可用 MyBatis 内置函数:WHERE name LIKE CONCAT('%', #{name}, '%') —— CONCAT 是数据库函数,#{name} 仍走预编译,安全。
根治 SQL 注入的关键不在写法,而在控制权归属
所有技术方案都建立在一个前提上:谁决定 SQL 的结构,谁就承担安全责任。#{} 只是把「数据值」的控制权交还给数据库预编译机制;${} 则是把「SQL 结构片段」的构造权完全让渡给应用层代码。
所以真正根治的落点是:
- 禁止任何用户输入直通
${}—— 表名、字段名、排序方式等,必须来自配置项、枚举或服务端白名单校验后的结果 - 禁用运行时拼接动态 SQL 片段(比如从 HTTP 参数读一个
column就塞进${column}) - 开启 MyBatis 的
logImpl=STDOUT_LOGGING,定期检查日志里有没有Preparing: SELECT * FROM ${...}这类高危模式
最危险的不是不懂 #{} 和 ${} 的区别,而是以为加了引号、做了 trim、写了正则就等于安全——只要结构控制权在不可信输入手里,所有补丁都是纸糊的。











