PreparedStatement 能防 SQL 注入是因为它将 SQL 模板与参数彻底分离,参数通过 setXxx() 安全传递而非拼接进 SQL 文本,即使含恶意字符串也仅作字面量处理。

为什么 PreparedStatement 能防 SQL 注入
因为 PreparedStatement 把 SQL 模板和参数彻底分离:数据库驱动先将含占位符(?)的 SQL 发送给 MySQL 服务端预编译,生成执行计划;后续调用 setString()、setInt() 等方法传入的值,**不会拼进 SQL 文本**,而是作为独立参数由 MySQL 协议安全传递。即使参数里含 ' OR 1=1 --,MySQL 也只当它是字符串字面量,不会参与语法解析。
Java 中 PreparedStatement 的正确写法
关键不是用了 PreparedStatement,而是**所有动态值都必须走 setXxx() 方法**,绝不能字符串拼接:
String sql = "SELECT * FROM users WHERE username = ? AND status = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, userInputName); // ✅ 正确:参数化 ps.setInt(2, statusValue); // ✅ 正确:类型明确 ResultSet rs = ps.executeQuery();
下面这些是典型错误:
-
"WHERE username = '" + userInput + "'"—— 字符串拼接,直接失效 -
"WHERE id IN (" + idListStr + ")"—— 即使用了PreparedStatement,但手动拼了IN列表 -
ps.setString(1, "admin' --")—— 这本身不危险,但若前面拼了 SQL 就完蛋
哪些场景容易漏掉 PreparedStatement
常见于动态条件构建、批量操作、表名/列名拼接等地方,而这些恰恰是注入高发区:
-
动态 WHERE 条件:不要拼 SQL,改用条件式占位符 + 逻辑跳过,例如
WHERE ( ? IS NULL OR name = ? ) -
IN 查询:无法直接用单个
?代替多个值,需动态生成对应数量的?,再逐个setXxx() -
表名/列名:
PreparedStatement不支持参数化标识符,必须白名单校验或枚举映射,绝不可直接插入用户输入 -
ORDER BY 字段:同上,用预设字段名映射,如
if ("name".equals(sort)) sql += " ORDER BY name"
注意 MySQL 驱动版本与服务器配置影响
旧版 MySQL Connector/J(如 5.1.x)默认关闭服务端预编译,实际走的是客户端模拟预编译,仍可能被绕过;建议:
- 升级到
mysql-connector-java 8.0+或mariadb-java-client - 连接 URL 加上
useServerPrepStmts=true&cachePrepStmts=true - 确认 MySQL 服务端
max_prepared_stmt_count足够(默认 16382),避免频繁创建销毁开销
真正难防的从来不是技术本身,而是把 PreparedStatement 当成护身符,却在别处悄悄拼 SQL。参数化不是开关,是贯穿每一行数据边界的习惯。











