{ } 是唯一能防 SQL 注入的写法:它通过 JDBC PreparedStatement 预编译,将参数作为独立数据绑定,数据库仅视其为字符串值;${ } 是纯字符串拼接,直接嵌入 SQL,无任何防护,极易引发注入攻击。

MyBatis里#{}和${}根本不是“写法偏好”,而是两条完全不同的SQL组装路径
前者走 JDBC PreparedStatement 预编译,后者是纯字符串拼接。这不是“哪个更安全”的选择题,而是“该不该让数据库执行你拼出来的这段 SQL”的判断题。
关键区别不在语法,而在执行时数据库看到的是什么:
-
#{id}→ 数据库收到的是SELECT * FROM user WHERE id = ?,再由驱动把值绑定进去 -
${table}→ 数据库收到的是SELECT * FROM user_log_202609,连解析都跳过,直接执行
哪些地方必须用${}?不是“能用”,而是“只能用”
当你要动态指定 SQL 结构本身——比如表名、列名、ORDER BY 字段、GROUP BY 表达式——JDBC 的 ? 占位符根本不支持这些位置。
常见必须用 ${} 的场景:
-
SELECT * FROM ${tableName}(分表场景) -
ORDER BY ${sortField} ${sortOrder}(前端传排序字段) -
IN (${ids})(注意:这不是正确写法,见下一条)
⚠️ 重点提醒:IN 子句不能简单写成 IN (${ids})。MyBatis 提供了 <foreach> 标签来安全生成参数列表,它内部仍用 #{} 填充每个值。
用${}时防 SQL 注入不是“加个校验就行”,而是要彻底控制输入源
你不能指望在 DAO 层对 ${column} 做 if (col.equals("name") || col.equals("age")) 这种白名单判断——一旦业务扩展,漏掉一个就崩。
真正可行的做法:
- 所有
${}参数必须来自枚举或预定义常量,禁止直接接收前端原始字符串 - 如需前端控制排序字段,后端应只接受字段编号或 key(如
"sort=1"),映射到内部白名单数组:["create_time", "update_time", "score"] - 表名动态拼接时,强制要求前缀/后缀规则(如固定为
"log_" + yyyyMM),不暴露任意命名权
别信“我过滤了单引号和分号就安全了”——绕过方式太多,UNION SELECT、注释符、编码混淆都能打穿这种弱防护。
#{}不是万能的,但它是默认且唯一安全的起点
绝大多数参数都应该无条件走 #{}:WHERE 条件值、UPDATE SET 字段、INSERT VALUES、LIKE 模糊匹配(记得自己加 %)。
特别注意两个易错点:
-
LIKE写法:name LIKE CONCAT('%', #{keyword}, '%')或name LIKE '%${keyword}%'❌ —— 后者直接开洞 -
IN正确写法:id IN <foreach item="id" collection="idList" open="(" separator="," close=")">#{id}</foreach>,不是${idList}
最危险的惯性思维,是把 ${} 当成“高级功能”去用。它其实是不得已的逃生通道,每次打开都要确认:这个通道是否真的不可替代?有没有更封闭的替代方案?

















