{}是防SQL注入唯一可靠方式,所有用户输入值必须用它;${}仅限白名单校验的表名、列名等非参数化场景。MyBatis将#{param}编译为JDBC ?占位符,实现结构与数据分离,自动类型绑定,杜绝拼接风险。

#{} 是唯一可靠的第一道防线,所有用户输入的“值”必须用它;${} 是高危开关,只在表名、列名、ORDER BY 等无法参数化的 SQL 片段中出现,且必须白名单校验。
所有 WHERE/INSERT/UPDATE 的值都必须用 #{}
这是防注入最核心、最不可妥协的规则。MyBatis 会把 #{param} 编译成 JDBC 的 ? 占位符,由数据库驱动完成类型绑定和转义——不是字符串拼接,而是结构与数据彻底分离。
- 安全写法:
WHERE name = #{name}→ 实际执行WHERE name = ?,参数值"admin' OR 1=1"就是纯字符串,不会触发逻辑绕过 - 危险写法:
WHERE name = '${name}'→ 直接拼接,输入admin' OR '1'='1会导致 SQL 变成WHERE name = 'admin' OR '1'='1' -
#{}自动适配类型:传int就调setInt(),传String就调setString(),无需手动加引号
like 模糊查询不能直接拼 %${xxx}%
前后模糊查询是注入重灾区,${} 拼接通配符等于主动开门。正确做法是让数据库或 MyBatis 完成拼接,参数本身仍走 #{}。
- 推荐方案(MySQL):
WHERE name LIKE CONCAT('%', #{name}, '%')——CONCAT在数据库端执行,#{name}仍是安全参数 - 兼容方案(多数据库):
<bind name="pattern" value="'%' + name + '%'" /> WHERE name LIKE #{pattern}——<bind>在 MyBatis 解析阶段生成变量,#{pattern}仍走预编译 - 绝对禁止:
WHERE name LIKE '%${name}%'或WHERE name LIKE '${pre}%${post}',任何${}出现在 LIKE 右侧都等同于裸奔
${} 只能用于白名单控制的 SQL 片段
当确实需要动态列名、表名或排序字段时,${} 不可避免,但必须剥夺其任意性。
- 白名单校验示例:
ORDER BY ${@com.example.util.SqlSafeUtil@checkOrderColumn(column)},方法内只允许返回"id"、"name"、"create_time"等硬编码值 - 枚举替代:
<choose><when test="sort == 'name'">ORDER BY name</when><when test="sort == 'id'">ORDER BY id</when></choose>,完全规避${} - 严禁将前端传入的任意字符串直接塞进
${tableName}或${columnName},哪怕加了正则也挡不住绕过
动态 SQL 标签本身不防注入,关键看内部怎么用
<if>、<where>、<foreach> 这些标签只是组装 SQL 结构的工具,它们的安全性完全取决于里面是否用了 #{}。
- 安全:
<if test="name != null">AND name = #{name}</if>—— 条件存在与否由 Java 表达式控制,值仍走预编译 - 危险:
<if test="name != null">AND name = '${name}'</if>—— 动态标签掩盖不了字符串拼接的本质 -
<foreach>中的 item 也必须用#{item},不能写${item},否则批量查询也会被注入
#{} 只保护“值”,不保护“结构”。一旦你把用户输入放进 ${},哪怕只有一处、哪怕加了 trim 或 length 判断,只要没做白名单或枚举约束,就等于在防火墙上凿了个洞。

















