为什么说jdbc的
PreparedStatement可以完全避免SQL注入的问题,还能高效利用数据库本身对查询的缓存?它的机制是什么,是数据库层面做的吗?
1 为什么能完全防范 SQL 注入?
JDBC Statement 中,SQL 语句是通过字符串拼接完成的。数据库拿到的是一串分不清哪里是“命令”、哪里是“数据”的字符。
-
普通 Statement 的漏洞:
如果你拼接SELECT * FROM users WHERE username = '+admin' OR '1'='1+',数据库看到的指令是: “选出所有用户,只要 1 等于 1 就行” 。数据篡改了逻辑。 -
PreparedStatement :
它采用了参数化查询(Parameterized Query) 。- 预编译阶段:驱动程序先向数据库发送一个带占位符的模版:
SELECT * FROM users WHERE username = ?。 - 数据绑定阶段:再把具体的参数(如
admin' OR '1'='1)传过去。
- 预编译阶段:驱动程序先向数据库发送一个带占位符的模版:
关键点:由于数据库已经提前完成了 SQL 的词法和语法分析,它已经认定这个 ? 位置只能是纯文本数据。哪怕你传入的数据里包含 DROP TABLE 或者 OR 1=1,数据库也只会把它当成一个“名字特别奇怪的用户”,而不会将其当作指令执行。
2 为什么能高效利用缓存?
这就涉及到数据库执行 SQL 的过程:解析(Parse) -> 优化(Optimize) -> 执行(Execute) 。
- 硬解析(Hard Parse) :如果每次 SQL 字符串都不同(比如 ID 一直变),数据库就得针对每一条 SQL 重新计算执行计划(怎么查最快、用哪个索引),这非常耗 CPU。
- 软解析(Soft Parse) :
PreparedStatement允许数据库缓存执行计划。
因为你发送的 SQL 模版始终是 SELECT * FROM items WHERE id = ?,数据库会发现:“诶,这个模版我见过,执行计划已经算好了,直接拿来用吧!”。
注意:这种缓存主要发生在数据库的 SGA(系统全局区)/ Library Cache 中,而不是在 Java 程序里。
3 它的机制是什么?是数据库层面做的吗?
这是一个端到端(客户端 + 服务端) 的配合过程,其核心协议通常被称为 二进制协议(Binary Protocol) 。
3.1 宏观流程:
-
客户端(JDBC Driver) :将带有
?的 SQL 发给数据库,并请求一个Statement ID。 -
服务端(Database) :
- 校验 SQL 语法。
- 生成执行计划。
- 将该计划与
Statement ID绑定并存入缓存。 - 返回 ID 给客户端。
-
客户端:调用
setInt()或setString(),将纯数据(不含 SQL 关键字)和Statement ID打包发送。 -
服务端:根据 ID 找到缓存好的计划,直接把数据塞进去运行。
3.2 是不是所有数据库都支持?
-
数据库支持:大多数现代关系型数据库(MySQL, PostgreSQL, Oracle, SQL Server)都在内核层面支持预编译。
-
驱动模拟:如果某些老旧数据库不支持,JDBC 驱动会在本地进行“模拟(Client-side emulation)”,即在本地安全地转义字符串再发过去。
- 冷知识:在 MySQL 中,默认情况下 JDBC 是在本地模拟预编译的。如果你想让它在数据库层面真正“硬核”执行,需要在连接字符串里加上
useServerPrepStmts=true。
- 冷知识:在 MySQL 中,默认情况下 JDBC 是在本地模拟预编译的。如果你想让它在数据库层面真正“硬核”执行,需要在连接字符串里加上
4 总结
PreparedStatement 的本质是把 SQL 变成了“带参数的函数” 。
- 安全性:数据永远不会被误认为指令(类型安全)。
- 性能:一次编译,多次运行(避免重复计算执行计划)。
稍微多说一句: 虽然它能防住 99% 的注入,但如果你在 SQL 模版里依然用字符串拼接表名(比如 SELECT * FROM ? 是不支持的,表名必须拼接),那还是会面临注入风险。这时候就需要你手动做严格的白名单校验了。