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 里做判断,参数引用仍用 #{}。

三、防注入要点

  1. 值一律 #{}, 禁止用 ${} 拼用户输入;
  2. 必须用 ${} 的位置做白名单:表名/列名/排序方向校验后从枚举取;
  3. 后端二次校验:接口层参数校验(长度、格式、枚举);
  4. 日志审计:SQL 只打参数占位形式,避免把完整 SQL 拼进日志;
  5. 代码评审红线:mapper XML 中出现 ${ 需说明用途且确认白名单。

常见追问 / 记忆点

  • 追问:${} 注入典型例子?答:name = '${name}' 传入 ' or '1'='1 会改写整条 SQL 语义,造成越权/拖库。
  • 追问:IN 查询怎么写才安全?答:<foreach> + #{id},由框架展开成多个占位符。
  • 记忆点:占位符 #{} 管“值”,字符串 ${} 管“结构”;结构需白名单,值永远占位符。
笔记加载中…