preparedstatement防sql注入的核心是sql结构与用户数据彻底分离:数据库先编译带?的模板并缓存执行计划,再通过二进制协议传入纯参数值,?仅接受列值且受强类型约束,杜绝恶意输入被解析为sql语法。

PreparedStatement 防 SQL 注入,核心在于SQL 结构与用户数据彻底分离——不是靠“转义字符”或“过滤单引号”,而是从数据库执行机制层面切断攻击路径。
数据库先编译模板,再传入参数
当你调用 conn.prepareStatement("SELECT * FROM user WHERE name = ?") 时,JDBC 驱动会把这条带问号的语句发给数据库。数据库立即做三件事:
- 解析语法,确认这是合法的 SELECT 语句
- 生成执行计划(比如走 name 索引)
- 缓存这个“结构固定”的模板,等待后续传参
此时还没有任何用户输入参与。等你调用 pstmt.setString(1, "admin' OR '1'='1"),驱动才通过独立的二进制协议把该字符串作为纯数据值传过去。数据库收到后,只把它当一个普通字符串值塞进 WHERE 条件,绝不会重新解析它是否含 SQL 关键字。
占位符 ? 只接受值,不接受语法成分
问号是 JDBC 规范定义的参数占位符,它只能代表列值(如字符串、数字、日期),不能代表表名、字段名、ORDER BY 方向、GROUP BY 子句等 SQL 结构部分。
- ✅ 正确:
"SELECT * FROM user WHERE id = ?"→setLong(1, userInputId) - ❌ 错误:
"SELECT * FROM " + tableName + " WHERE id = ?"→ 表名拼接即破防 - ❌ 错误:
"ORDER BY ? DESC"→ ? 无法绑定列名,运行时报错或被当作字符串字面量
类型绑定强制语义约束
每个 setXxx() 方法不只是赋值,还明确告诉数据库:“这个参数按 JDBC 类型 XXX 处理”。例如:
-
setInt(1, "123")会报错(类型不匹配) -
setString(1, "123")传给 INT 字段 → 数据库可能隐式转换,但仍是作为值处理,不会触发语法解析 -
setNull(1, Types.INTEGER)显式传空值,避免setInt(1, null)报 NPE
这种强类型契约进一步压缩了恶意输入被误解释为指令的空间。
对比 Statement:拼接即失控
用 Statement 时,SQL 字符串在 Java 层就已拼好,比如:
"SELECT * FROM user WHERE name = '" + input + "'"一旦 input 是 "alice' --",最终发送给数据库的就是完整可执行语句:
SELECT * FROM user WHERE name = 'alice' --'
注释符直接吞掉后面校验逻辑——数据库根本没机会区分哪是模板、哪是数据。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











