SQL 如果靠字符串拼用户输入,结构就会被输入改掉。预编译把语句结构和参数分开:结构先固定,参数后绑,这是防注入的正路。
JDBC
java
String sql = "SELECT name, age FROM student WHERE id = ?";
try (Connection con = dataSource.getConnection();
PreparedStatement ps = con.prepareStatement(sql)) {
ps.setString(1, id);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
System.out.println(rs.getString("name") + " " + rs.getInt("age"));
}
}
}? 是占位符。setString(1, id) 给第一个问号赋值,下标从 1 开始。不要再把 SQL 拼进 executeQuery。
拼接的反例:
java
// 不要这样
String sql = "SELECT * FROM student WHERE name = '" + userInput + "'";userInput 里一旦出现 ' OR '1'='1,WHERE 的含义就变了。PreparedStatement 会把输入当数据,不当代码。
MyBatis 里的 #{} 和 ${}
#{} 走预编译,最终变成 JDBC 的 ?,会按类型设值,字符串会带引号。${} 是字符串替换,解析阶段就把变量写进 SQL,不带预编译。
sql
-- id = 1 时
SELECT * FROM student WHERE id = #{id}
-- 近似:WHERE id = ?
SELECT * FROM student WHERE id = ${id}
-- 近似:WHERE id = 1能用 #{} 就用 #{}。ORDER BY 列名、动态表名这类不能参数化的片段,才不得不 ${},并且列名必须来自白名单,不能来自用户原文。
记住:#{} 是参数,${} 是插值。插值发生在编译前,注入也发生在编译期。