PreparedStatement能防SQL注入,因其在数据库层面将SQL结构与参数物理隔离:预编译生成固定语法树,执行时参数仅作为纯数据绑定,不参与SQL解析;字符串拼接则使用户输入直入解析器,必然导致注入。

只要所有用户输入都走 PreparedStatement 的 setXXX() 方法,SQL 注入就不可能发生——这不是“大概率防住”,而是 JDBC 协议层切断了数据被当作代码执行的路径。
为什么拼字符串 + replace() 或正则过滤根本没用
攻击者不需要用单引号或分号就能触发注入。比如 MySQL 中输入 admin' -- 、1 OR 1=1、甚至 test%00(空字节截断)都可能绕过简单过滤;更隐蔽的是利用 LIKE 中的 % 和 _ 实现盲注,或借助堆叠查询(如 1; DROP TABLE users)在允许的配置下直接执行多语句。
手动转义(如 replace("'", "''"))本质仍是字符串拼接,只要 SQL 结构里混入了未受控输入,就等于把解析权交给了攻击者。数据库看到的是一整条被污染的语句,不是“带参数的模板”。
- 所有用户可控输入都必须进
?占位符,包括登录名、邮箱、状态码、时间范围、ID、搜索关键词 - 数字类型也不能例外:用
pstmt.setInt(1, userId),而非"WHERE id = " + userId - 哪怕只查一个固定字段,也得写
"SELECT name FROM user WHERE id = ?",不能图省事拼接
PreparedStatement 正确用法:三步缺一不可
光有 prepareStatement() 调用还不够,必须同时满足:SQL 字符串不含任何用户输入、占位符顺序与 setXXX() 严格对应、类型匹配。
立即学习“Java免费学习笔记(深入)”;
- SQL 字符串必须是纯模板:
"INSERT INTO order (uid, amount, status) VALUES (?, ?, ?)"—— 不能含"'"+status+"'" - 设置参数必须用类型化方法:
pstmt.setLong(1, userId)、pstmt.setString(2, "99.99")、pstmt.setString(3, "paid") - 索引从 1 开始,不能错位;
setObject(1, value)虽然能用,但会丢失类型提示,建议优先用setString/setInt等
错误示例:"SELECT * FROM user WHERE name = '" + name + "'" —— 即使加了 Objects.requireNonNull(name) 或 name.trim(),依然危险。
当用户要求为 Java 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 Java 注释风格。
哪些地方不能参数化?白名单是唯一解
表名、列名、排序字段(ORDER BY ?)、分组字段(GROUP BY ?)、UNION 子句等语法位置,SQL 标准不允许用 ? 占位。这些必须靠白名单校验,不能靠“看起来安全”。
- 动态表名只能从预设集合中取:
if (!List.of("user", "order", "log").contains(tableName)) throw new IllegalArgumentException(); - 排序字段只允许
"id"、"created_at"、"status"等明确枚举值,不能接受任意字符串 - 原生 SQL 查询(如 JPA 的
@Query(nativeQuery = true))同样适用该规则,ORM 不自动兜底
别信“前端已限制输入”——HTTP 请求可被任意构造,后端才是最后一道防线。
连接池和预编译开关不是防注入的关键
Druid 默认启用客户端预编译,HikariCP 不干预,MySQL 服务端预编译(useServerPrepStmts=true)反而容易因 max_prepared_stmt_count 触发 HY000 / 1461 错误,且性能通常更差。真正起作用的只有两件事:
- 你是否调用了
connection.prepareStatement(sql) - 你是否把所有用户输入都塞进了
setXXX(),而不是拼进sql字符串
即使连接池配置再“高级”,只要写了 statement.executeQuery("SELECT * FROM user WHERE name = '" + input + "'"),就等于完全放弃防御。
最容易被忽略的是那些“不显眼”的输入点:导出 Excel 的时间范围、后台管理的模糊搜索关键词、分页的 limit 参数、甚至 HTTP Header 里传来的 trace-id 如果被记入日志表并用于查询 —— 只要进 SQL,就必须参数化。

















