PreparedStatement 是防止 SQL 注入最直接可靠的方式——参数必须全部通过 setXxx() 方法绑定,SQL 模板中动态值仅限用 ? 占位,严禁拼接用户输入到 SQL 字符串中,否则仍存在注入风险。

预编译语句 PreparedStatement 是防止 SQL 注入最直接、最可靠的方式——只要参数全部通过 setString()、setInt() 等方法绑定,SQL 结构和数据就彻底分离,数据库不会把用户输入当语法解析。
为什么拼接字符串 + Statement 一定危险
常见错误是用 String.format() 或 + 拼接用户输入进 SQL:
String sql = "SELECT * FROM users WHERE name = '" + userName + "'";
一旦 userName 是 "admin' -- ",整条语句变成:
SELECT * FROM users WHERE name = 'admin' -- '
注释掉后续校验逻辑,可能绕过登录、删库、拖走敏感字段。这类漏洞不依赖数据库版本或配置,纯代码层缺陷。
PreparedStatement 的正确写法和关键约束
必须满足两个条件才真正安全:
- SQL 模板中所有动态值都用
?占位,不能出现在表名、列名、ORDER BY子句等位置 - 所有用户输入都通过
setXxx()方法注入,绝不用字符串拼接塞进 SQL 字符串里
示例(安全):
String sql = "SELECT id, name FROM users WHERE status = ? AND created_time > ?";<br>PreparedStatement ps = conn.prepareStatement(sql);<br>ps.setString(1, "active");<br>ps.setTimestamp(2, Timestamp.from(Instant.now().minusSeconds(86400)));
如果真要动态列名(如按不同字段排序),只能白名单校验后拼接,不能交给 PreparedStatement 处理。
容易被忽略的坑:类型匹配与空值处理
PreparedStatement 不只是防注入,还强制类型语义。类型错配会报错或隐式转换失败:
- 对数字字段用
setString()可能触发数据库类型转换异常,尤其 PostgreSQL 严格检查 - 传
null必须用setNull(index, Types.INTEGER),不能直接setInt(1, 0)冒充 - 时间字段优先用
setTimestamp()而非setString(),避免时区或格式歧义
这些细节不影响防注入能力,但会导致运行时报错或查不到数据,常被当成“预编译没用”误判。
真正的难点不在写 PreparedStatement,而在于团队是否统一约束了所有 DAO 层入口——只要有一处漏掉,整套防护就形同虚设。另外 ORM 框架(如 MyBatis)默认用预编译,但若写了 ${} 替换而非 #{},照样注入。











