需要明确的是:在 MyBatis 框架中,真正能够有效防御 SQL 注入攻击的只有 #{} 写法。而 ${} 本质上是直接进行字符串拼接,并不提供任何防护能力——传入什么就原样拼接到 SQL 中,最终数据库接收到的是完整的 SQL 文本,而非参数化的查询。一旦攻击者发现某个字段未经严格校验,就可以利用 ' OR '1'='1 或 ; DROP TABLE user; -- 执行任意恶意语句。这绝非危言耸听。

#{ } 是唯一能有效防范 SQL 注入的写法
在 MyBatis 中,只有 #{} 能够实现真正的 SQL 注入防御,而 ${} 本身不附带任何安全机制——它只是纯粹地进行字符串拼接,参数值会被直接嵌入到 SQL 语句中。数据库接收到的不是参数占位符,而是完整的可执行 SQL 字符串。即便只遗漏一个字段的校验,攻击者也有可能利用 ' OR '1'='1 或 ; DROP TABLE user; -- 执行任意操作。这并非设计缺陷,而是其底层工作方式决定的。
为什么 #{ } 能够成功拦截恶意输入?
核心在于 #{} 会启动 JDBC 的 PreparedStatement 机制,该机制天生具备防注入特性:
- MyBatis 先将
#{id}替换为?占位符,SQL 语句发送到数据库时结构已经固定,例如SELECT * FROM user WHERE id = ? - 参数值通过
setLong(1, id)或setString(1, value)单独绑定,数据库仅将其视为数据值,不会作为语法解析 - 即使传入
"1; DROP TABLE user; --",实际执行的仍是WHERE id = '1; DROP TABLE user; --'—— 仅仅是一条无法查出结果的普通查询
可以想象,数据库接收到的是已经定型的 SQL 骨架,参数仅仅是被填充的数据值,无论怎样尝试都无法改变查询结构。
哪些场景不得不使用 ${ }?
需要说明的是,使用 ${} 并非出于偷懒,而是 JDBC 层面根本不允许用 #{} 的硬性场景:
- 表名:
SELECT * FROM ${tableName}—— 如果使用#{tableName}会添加单引号导致变成FROM 'user',直接引发语法错误 - 列名或排序字段:
ORDER BY ${column} ${orderType}——#{orderType}会变成ORDER BY name 'ASC',同样报错 - GROUP BY、LIMIT 偏移量等无法添加引号的语法位置
这些场景使用 ${} 并非偷懒,而是别无选择。既然必须使用,就必须将安全措施落实到位。
使用 ${ } 时最容易被忽略的安全措施
很多人认为“加个正则校验”或“限制输入长度”就足够了,实际上还远远不够。真正稳妥的做法至少需要执行以下步骤:
- 白名单必须硬编码:例如排序字段仅允许
Set.of("id", "name", "created_at"),前端传入sort=user_name,后端映射为真实列名"name" - 表名需要双重校验:先匹配正则
^[a-zA-Z][a-zA-Z0-9_]{2,31}$,再查询数据库元信息确认该表确实存在 - 绝对禁止用户直接传递字段名或表名:所有动态片段都应来自配置或枚举,而不是前端的原始输入
如果不执行这些步骤,${column} 与直接使用 Statement 没有本质区别,风险完全暴露。务必牢记:凡是能用 #{} 的地方绝不使用 ${},在必须使用 ${} 的场合,每增加一层校验就多一份安全保障。
