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 结构"成为铁律。

笔记加载中…