MyBatis #{} 与 ${} 有何区别?动态 SQL 如何防注入?
结论先行:#{} 走 PreparedStatement 占位符(预编译,参数安全),永远用它传值;${} 是字符串直接拼接进 SQL,只能用于表名/列名/排序方向等无法占位的位置,且必须白名单校验。动态 SQL 用 <if>/<where>/<foreach> 等标签在 XML 中拼装,与 ${} 无关——动态拼结构安全,参数拼接用 #{} 才安全。
一、核心区别
<!-- #{} 安全:框架生成 ? 并绑定参数 -->
SELECT * FROM user WHERE name = #{name}
<!-- ${} 危险:直接替换进 SQL 文本 -->
SELECT * FROM user WHERE name = '${name}'
| 写法 | 处理方式 | 注入风险 | 适用 |
|---|---|---|---|
| #{} | PreparedStatement 占位符 | 无 | 值参数(where、set、insert 值) |
| ${} | 纯字符串拼接 | 高 | 表名、列名、ORDER BY 方向 |
二、动态 SQL 常用标签
<select id="findUsers" resultType="User">
SELECT * FROM user
<where> <!-- 自动处理前置 AND/OR -->
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
<choose>
<when test="orderBy == 'create_time'">ORDER BY create_time DESC</when>
<otherwise>ORDER BY id</otherwise>
</choose>
<foreach collection="ids" item="id" open="AND id IN (" separator="," close=")">
#{id}
</foreach>
</select>
- <where> 智能去除多余 AND/OR;<set> 用于 update 动态列;<foreach> 处理 IN 集合;
- OGNL 表达式写在 test 里做判断,参数引用仍用 #{}。
三、防注入要点
- 值一律 #{}, 禁止用 ${} 拼用户输入;
- 必须用 ${} 的位置做白名单:表名/列名/排序方向校验后从枚举取;
- 后端二次校验:接口层参数校验(长度、格式、枚举);
- 日志审计:SQL 只打参数占位形式,避免把完整 SQL 拼进日志;
- 代码评审红线:mapper XML 中出现
${需说明用途且确认白名单。
常见追问 / 记忆点
- 追问:${} 注入典型例子?答:
name = '${name}'传入' or '1'='1会改写整条 SQL 语义,造成越权/拖库。 - 追问:IN 查询怎么写才安全?答:<foreach> + #{id},由框架展开成多个占位符。
- 记忆点:占位符 #{} 管“值”,字符串 ${} 管“结构”;结构需白名单,值永远占位符。