PreparedStatement能防注入,因其将SQL语句模板与用户参数彻底分离:数据库先预编译含?占位符的语句,再将输入作为纯数据绑定,不参与语法解析,故恶意字符串如"admin' OR '1'='1"仅被视作值而非代码。

为什么 PreparedStatement 能防注入
因为 PreparedStatement 把 SQL 语句和参数彻底分开处理:数据库先编译好带占位符(如 ?)的语句模板,再把用户输入作为纯数据绑定进去,不会参与语法解析。哪怕你传入 "admin' OR '1'='1",它也只会被当做一个字符串值,不会拼进 SQL 逻辑里。
这和直接用字符串拼接(比如 "SELECT * FROM user WHERE name = '" + name + "'")有本质区别——后者等于把用户输入直接喂给 SQL 解析器,风险完全不可控。
Java 中 PreparedStatement 的正确写法
关键不是“用了没”,而是“用对没”。常见错误包括手动拼接、漏设参数、类型不匹配:
- 必须用
?占位符,不能在 SQL 字符串里拼接变量 - 所有参数都得通过
setString()、setInt()等方法绑定,不能靠toString()或字符串替换 - 占位符数量和
setXxx()调用次数必须严格一致,否则抛SQLException - 批量操作要用
addBatch()+executeBatch(),别循环创建新PreparedStatement
示例:
String sql = "SELECT * FROM users WHERE status = ? AND created_at > ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setInt(1, 1); // status = 1 ps.setTimestamp(2, timestamp); // 时间戳类型要匹配
哪些情况 PreparedStatement 也防不住
它只保护「参数值」,不保护「SQL 结构」。以下场景仍可能被注入:
- 表名、字段名、排序字段(
ORDER BY后的内容)动态拼接:这些无法用?占位,必须白名单校验或枚举 - 拼接
IN子句(如WHERE id IN (?, ?, ?)):参数个数不确定时,得动态生成占位符,但生成逻辑本身不能拼用户输入 - 使用
setObject()且传入恶意构造的自定义对象:极少见,但若 JDBC 驱动存在反序列化漏洞,可能绕过
例如 "SELECT * FROM " + tableName + " WHERE id = ?" —— 表名 tableName 若来自用户请求且未校验,就直接失效。
和 ORM 框架混用时的注意点
像 MyBatis、Hibernate 默认走预编译,但留了“方便入口”让人掉坑:
- MyBatis 的
${}是字符串替换,#{}才是预编译参数 —— 写成SELECT * FROM ${table}就等于裸奔 - Hibernate 的
createNativeQuery()如果拼接 SQL,同样不自动预编译;要用setParameter()显式绑定 - JPA 的
@Query(nativeQuery = true)注解下,占位符必须是?或命名参数(:name),不能混用字符串拼接
本质上,只要最终落到 JDBC 层的是一个带 ? 的固定 SQL 模板,并由 PreparedStatement 执行,才真正受保护。
实际项目中最容易被忽略的,是那些看似“只是拼个表名”“只是动态加个字段”的地方——它们不在参数范围内,却常被当成普通变量一样信任。











