SQL 注入防护:底层预编译机制与安全写法
SQL 注入的本质是"用户输入变成了 SQL 的一部分"。MyBatis 相对安全,是因为它默认走预编译:#{} 编译成占位符 ?,值由 JDBC 驱动转义后单独传入。但 #{} 并非万能,${} 直拼、表名列名动态化仍是重灾区。本节讲清机制与安全写法。
#{} 的预编译机制
<select id="selectByName" resultType="User">
SELECT * FROM user WHERE name = #{name}
</select>
执行时 MyBatis 先把 SQL 解析成:
SELECT * FROM user WHERE name = ?
再把参数交给 PreparedStatement.setString 传入。用户的 ' OR '1'='1 只会被当成一个普通字符串值,永远无法闭合 SQL 结构——值与 SQL 文本彻底分离,这正是预编译防注入的底层原理。
拼接与占位符对比
| 写法 | 生成方式 | 安全性 |
|---|---|---|
#{value} | 占位符 ? + 参数绑定 | 安全,值无法改变 SQL 结构 |
${value} | 直接字符串替换进 SQL | 危险,等价手工拼接 |
// 危险:用户输入直接进入 SQL 文本
// SELECT * FROM user WHERE name = ${name}
// 传入 x' OR '1'='1 时整条条件被改写,可能拖出全部数据
必须用 ${} 的场景与白名单
表名、列名、ORDER BY 字段属于"SQL 结构",占位符传不了,只能用 ${}——但值必须白名单校验:
private static final Map<String, String> SORT_WHITELIST = Map.of(
"id", "id", "createTime", "create_time", "name", "name");
public String safeColumn(String input) {
String col = SORT_WHITELIST.get(input); // 不在白名单返回 null
if (col == null) throw new IllegalArgumentException("非法排序列");
return col; // 只允许返回常量
}
ORDER BY ${sortColumn} <!-- sortColumn 已过白名单校验 -->
高危写法清单与修复
| 危险写法 | 正确写法 |
|---|---|
LIKE '%${kw}%' | LIKE CONCAT('%', #{kw}, '%'),% 由函数拼接 |
IN (${ids}) | <foreach collection="ids" item="i" open="(" separator="," close=")">#{i}</foreach> |
ORDER BY ${userInput} | 白名单映射后 ${safeColumn} |
| 手拼 AND/OR 表达式 | <where>/<if> 等动态 SQL 标签 |
补充防线:让 ${} 出现的位置能一眼数清(代码评审重点);数据库账号给最小权限、禁止应用直连超级用户,纵深防御以官方安全文档为准。
MyBatis 的安全底座是 #{} 预编译,真正要防的是自己写出的 ${}:能用 #{} 绝不用 ${},非用不可的结构性片段必须过白名单,让"用户输入只能当值、永远当不了 SQL 结构"成为铁律。