preparedstatement能防sql注入,因其将sql模板与参数物理隔离:数据库先预编译含?的语句生成固定语法树,再将用户输入作为纯数据绑定,不参与解析;而statement字符串拼接使输入直入sql解析器,易被恶意构造改变逻辑。

直接用 PreparedStatement,且全程不拼接 SQL 字符串——这是唯一能真正切断 SQL 注入链路的做法。其他手段(如过滤关键词、前端校验、ORM 框架)都是辅助或有前提的补丁,不能替代这条底线。
为什么 PreparedStatement 能防注入,而 Statement 不能
数据库在执行 PreparedStatement 前,已把 SQL 语句结构(语法树)固定下来;后续调用 setXxx() 只是往预留的“数据槽”里填值,这些值永远不参与语法解析。而 Statement 是把整个字符串交给数据库去“编译+执行”,用户输入一旦混进字符串,就可能变成 OR 1=1 -- 这类破坏性片段。
常见错误现象:
- 用
String sql = "SELECT * FROM user WHERE name = '" + name + "'"+statement.executeQuery(sql)→ 任何单引号、分号、括号都可能逃逸 - 即使对输入做了
name.replace("'", "''"),也挡不住1; DROP TABLE user--这类多语句攻击
PreparedStatement 的正确写法和典型陷阱
核心原则:SQL 字符串里只允许出现 ? 占位符,所有变量必须通过 setXxx() 绑定。
Java JDK 25 来自 OpenJDK 官方归档,版本为 JDK 25,本条下载地址已指向官方 Windows x64 zip 安装包直链,适合调试旧项目或兼容旧版 Java 运行环境。
- ✅ 正确:
"SELECT * FROM order WHERE status = ? AND created_at > ?"→ 后续调用setInt(1, 1)、setTimestamp(2, ts) - ❌ 错误:
"SELECT * FROM " + tableName + " WHERE id = ?"→ 表名拼接,预编译完全失效 - ⚠️ 动态
IN列表不能写成WHERE id IN (?);必须按实际个数生成占位符,如IN (?, ?, ?),再循环调用setLong() - ⚠️
ORDER BY后的字段名、GROUP BY列、LIMIT数值等“结构部分”不能用?,必须走白名单校验(例如只允许"name"、"created_at"、"desc")
参数类型必须严格匹配,否则可能出错或绕过防护
数据库靠类型信息决定如何序列化/比较该参数。用错 setXxx() 方法,轻则查不到数据,重则触发隐式转换,让防护形同虚设。
- INT 字段(如
id)→ 必须用setInt()或setLong(),不用setString("123") - TIMESTAMP 字段 → 用
setTimestamp(),传字符串会报错或存成 1970-01-01 - 要设
null→ 必须用setNull(index, Types.INTEGER),不能对setInt()传null(抛NullPointerException) - 批量操作时复用同一
PreparedStatement,每次循环只改参数、调addBatch(),别反复prepareStatement()
ORM 框架不是银弹,#{} 和 ${} 容易混淆
MyBatis 的 #{} 底层仍是 PreparedStatement,安全;但 ${} 是纯字符串替换,和 Statement 拼接无异,常用于动态表名、排序字段等场景——这些地方必须手动白名单校验。
- Hibernate/JPA 的 HQL 或 Criteria 查询默认安全,但一旦写原生 SQL(
@Query(value = "...", nativeQuery = true)),就必须自己用setParameter(),否则照样中招 - Spring Data JPA 的方法命名查询(如
findByUsernameAndStatus())是安全的,但@Query("SELECT * FROM user WHERE " + condition)这种拼接方式等于开门揖盗
最常被忽略的一点:表名、列名、函数名、排序方向这些“SQL 结构元素”,从来不在 PreparedStatement 的保护范围内。它们必须经过硬编码白名单或正则严格约束,而不是依赖“用户输入看起来像字段名”这种模糊判断。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










