PreparedStatement能防SQL注入,因其将SQL模板与参数分离,预编译后仅通过setXxx()传入纯数据,避免参数被解析为SQL代码;关键须全程禁用字符串拼接,且注意IN子句、表名列名等动态部分需白名单校验。

PreparedStatement 为什么能防 SQL 注入
因为它把 SQL 结构和数据彻底分开:SQL 模板在 prepareStatement() 阶段就编译好,后续 setString()、setInt() 等方法只传纯数据,数据库不会把参数当 SQL 片段解析。
常见错误现象:String sql = "SELECT * FROM user WHERE name = '" + name + "'"; 这种拼接,一旦 name 是 "admin' -- ",就直接绕过密码校验。
使用场景:所有带用户输入的 CRUD 操作,尤其登录、搜索、批量更新。
性能影响:首次执行稍慢(预编译开销),但重复执行时更快,因为数据库可复用执行计划。
立即学习“Java免费学习笔记(深入)”;
怎么写才真正生效(不是光用了 PreparedStatement 就安全)
关键点在于:参数必须全部通过 setXxx() 方法传入,不能在 SQL 字符串里拼接任何变量。
- ✅ 正确:
String sql = "SELECT * FROM user WHERE id = ? AND status = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setInt(1, userId); ps.setString(2, status); - ❌ 错误:
String sql = "SELECT * FROM user WHERE id = " + userId + " AND status = '" + status + "'"; PreparedStatement ps = conn.prepareStatement(sql);—— 这根本没用上预编译,只是套了个壳 - ⚠️ 注意:
IN子句不支持单个?代替多个值,WHERE id IN (?)只能匹配一个数;动态个数要用循环拼占位符或改用其他方案
哪些地方容易漏掉、导致白用 PreparedStatement
最常踩的坑是“半截子预编译”——只对部分参数用了 ?,其余仍字符串拼接。
- 表名、列名、排序字段(如
ORDER BY ?)不能用?占位,会报SQLException: Parameter index out of range或直接当字符串字面量处理 - 解决办法:白名单校验 + 字符串拼接(例如只允许
status、created_at出现在ORDER BY后) - 动态条件(比如搜索框可选填姓名/邮箱/手机号):别硬凑一个大 SQL,用 StringBuilder 拼 WHERE 片段,再用
PreparedStatement绑定确定的参数 - 批量操作别用
addBatch()+ 拼接 SQL 字符串,要用ps.addBatch()配合多次setXxx()
MySQL 和 PostgreSQL 在 setTimestamp 的行为差异
时间类型处理不当会导致查询结果错乱,不是注入,但破坏业务逻辑,常被忽略。
- MySQL 驱动默认把
java.util.Date当成 UTC 处理,若应用服务器时区 ≠ 数据库时区,setTimestamp(1, date)可能偏移几小时 - PostgreSQL 驱动更严格,
setTimestamp()默认丢弃时区信息,建议显式传Calendar:ps.setTimestamp(1, ts, Calendar.getInstance(TimeZone.getTimeZone("UTC"))) - 更稳妥做法:统一用
LocalDateTime+setObject(1, localDateTime, JDBCType.TIMESTAMP)(JDBC 4.2+)
复杂点在于:防注入不只是“用了 PreparedStatement”,而是整个参数流动路径是否干净——从 HTTP 请求进来到最终绑定,中间任何一次字符串拼接、反射取名、日志打印都可能破防。细节松一扣,前面全白干。










