PreparedStatement是防止SQL注入唯一可靠方案,因其将SQL模板与参数物理隔离:数据库先预编译含?的语句生成固定语法树,再将用户输入作为纯数据绑定,不参与解析;字符串拼接则使输入直入SQL解析器,必然导致注入。

用 PreparedStatement 是唯一可靠、可落地的方案。其他所谓“过滤关键字”“转义单引号”都是补丁式防御,漏掉一个字符或绕过一处逻辑就全盘失效。
为什么 Statement 拼接字符串必然出问题
只要 SQL 字符串里出现 +、String.format() 或 StringBuilder.append() 拼入用户输入,就等于把数据库的语法解析权交给了攻击者。
常见错误现象:
- 用户输
' OR 1=1 --,登录直接绕过 - 输
a'; DROP TABLE user; --,表被删 - 看似加了
replaceAll("'", "''"),但 MySQL 支持双字节编码绕过,或用反斜杠+空格等变体逃逸
根本原因:SQL 解析器在执行前才做词法分析,拼进去的字符串早已混入语法结构里,数据库无法区分哪部分是“指令”、哪部分是“数据”。
立即学习“Java免费学习笔记(深入)”;
PreparedStatement 的正确用法和硬性规则
它不是“用了就安全”,而是必须满足四个条件才能真正生效:
-
?占位符只能用于**值(value)位置**:WHERE 条件、INSERT VALUES、UPDATE SET 右侧等。不能用于表名、列名、ORDER BY 字段、GROUP BY 表达式 - 所有参数必须走
setXxx()方法绑定,且类型严格匹配:INT 字段用setInt(),时间字段用setTimestamp(),null 值用setNull(index, Types.INTEGER) - 禁止任何字符串拼接:哪怕只拼一个表名、一个字段名,整个预编译防护即刻失效
- 动态结构(如多条件搜索、排序字段)必须用白名单校验:先判断输入是否在
Arrays.asList("name", "email", "created_at")中,再拼进 SQL
示例(安全):
String sql = "SELECT * FROM user WHERE status = ? AND created_at > ? ORDER BY ? DESC";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setInt(1, 1);
ps.setTimestamp(2, startTime);
// ✅ 下面这行是错的!ORDER BY 后不能用 ?
// ps.setString(3, "name");
// ❌ 正确做法是白名单后拼接:
if (!Arrays.asList("name", "email", "created_at").contains(sortField)) {
throw new IllegalArgumentException("Invalid sort field");
}
sql = "SELECT * FROM user WHERE status = ? AND created_at > ? ORDER BY " + sortField + " DESC";IN 子句和批量操作的典型陷阱
WHERE id IN (?) 是常见误解——? 只代表单个值,数据库不会把它展开成多个参数。
正确做法是按实际数量动态生成占位符:
- 传入 3 个 ID → 构造
WHERE id IN (?, ?, ?) - 然后循环调用
setLong(i, ids.get(i-1)) - 别用
String.join(",", ids)拼字符串,那又回到拼接老路
批量插入必须复用同一个 PreparedStatement 实例,每次只改参数、调 addBatch(),最后 executeBatch()。不能每条记录都 prepareStatement() 一次,否则失去预编译意义,也容易触发连接池或驱动的参数上限。
最容易被忽略的边界点
很多团队在测试环境跑通了,上线后突然报错或查不到数据,往往卡在这几个地方:
- MySQL 8 驱动要求 URL 必须带
serverTimezone=UTC,否则setTimestamp()可能存成 1970-01-01 - Oracle 对
NULL处理更严格:用setString(1, null)会报错,必须用setNull(1, Types.VARCHAR) - 某些旧版驱动对超长参数(比如 > 4000 字符的 CLOB)支持不一致,
setString()可能截断或抛异常 - MyBatis 里误用
${}替代#{},本质还是字符串拼接,和Statement无异
真正的防护不在“有没有用 PreparedStatement”,而在“有没有人敢动那根弦”——只要代码里出现一次拼接、一次类型错配、一次白名单漏判,前面所有努力就归零。


















