底层机制:预编译与字符串拼接的安全边界
MyBatis 中 #{} 与 ${} 的核心差异,源于它们处理 SQL 参数的方式截然不同。#{} 采用预编译参数绑定(PreparedStatement),MyBatis 在解析 SQL 时会将占位符替换为数据库驱动的问号“?”,随后由 JDBC 驱动在数据库层进行安全的类型转换与转义。这意味着无论用户输入包含何种特殊字符,数据库都将其视为纯文本值,而非可执行的 SQL 指令。相反,${} 执行的是纯粹的字符串拼接,MyBatis 会在 SQL 编译前直接将变量值替换到 XML 或注解的文本中。例如,当传入参数为 admin' OR '1'='1 时,#{} 生成的 SQL 仍为安全的 WHERE name = ?,而 ${} 则会直接拼接到语句中,破坏原有 SQL 结构。这种机制差异直接决定了两者在面对不可信用户输入时的安全边界:#{} 天然具备防注入能力,而 ${} 则将拼接风险完全暴露给开发者。

风险剖析:为什么 ${} 是 SQL 注入的高发区
${} 之所以成为 SQL 注入的高发区,根本原因在于它绕过了数据库的预编译保护,直接将外部输入嵌入到 SQL 语句的语法结构中。当开发者在动态查询中误用 ${} 处理用户可控参数时,恶意输入即可轻易闭合原有引号并追加非法指令。典型危险场景包括:将前端传入的排序字段直接用于 ORDER BY ${column},或将搜索关键词拼接到 LIKE '%${keyword}%' 中。若攻击者传入 id; DROP TABLE users; --,数据库将直接执行破坏性操作。需要明确的是,#{} 并非万能替代品。由于预编译机制的限制,#{} 只能用于绑定 SQL 语句中的“值”(Value),无法用于替换表名、列名、排序关键字(ASC/DESC)或动态拼接的 SQL 片段。在这些必须动态生成 SQL 结构的场景中,盲目替换为 #{} 会导致语法错误,因此开发者必须正视 ${} 的不可替代性,并在必须使用时建立严格的输入过滤机制。

实战选型:值用 #{},结构用 ${} 且严控
在实际开发中,参数选型应遵循“值用 #{},结构用 ${} 且严控”的核心原则。对于所有 WHERE 条件过滤、INSERT/UPDATE 的字段赋值以及 LIKE 模糊查询,必须无条件使用 #{},以利用预编译机制彻底阻断注入路径。当业务需求涉及动态表名、动态列名、排序方向(ASC/DESC)或分页偏移量时,由于数据库预编译协议不支持对 SQL 标识符进行参数化,只能使用 ${}。此时必须引入白名单校验机制:例如在排序场景中,预先定义允许排序的字段集合 Set

验证与避坑:从 SQL 日志到安全测试
验证 SQL 安全性不能仅依赖代码审查,必须结合运行期日志与主动测试。首先,在开发环境开启 MyBatis 的 SQL 日志打印(如配置 logging.level.org.mybatis=DEBUG),观察实际发送给数据库的完整 SQL 语句。若日志中显示 #{} 被替换为带引号的字符串且无语法异常,说明绑定正常;若 ${} 拼接后出现未闭合引号或多余关键字,则存在注入隐患。其次,应使用边界值与标准注入 Payload(如 ' OR '1'='1、admin'; DROP TABLE users;--)进行自动化单元测试,验证恶意输入是否被拦截或转义。在避坑实践中,常见误用包括在 WHERE 子句中滥用 ${}、未对动态列名做枚举校验、以及将 ${} 与

