正确使用 preparedstatement 防 sql 注入的关键是所有用户输入必须通过 setxxx() 绑定,确保 sql 结构与数据分离;表名、列名等结构化内容须白名单校验,禁止字符串拼接,且需复用实例以发挥性能优势。

用对 PreparedStatement 才能真正防住 SQL 注入,也才能发挥它的性能优势。关键不在“用了没”,而在“怎么用”。
所有用户输入必须走 setXxx() 绑定
PreparedStatement 防注入的原理是:SQL 结构和数据彻底分离。数据库先解析固定结构的语句,再把参数值通过二进制通道传入——值永远被当数据处理,不会参与语法解析。
- 正确写法:conn.prepareStatement("SELECT * FROM user WHERE name = ? AND status = ?"),再调用 pstmt.setString(1, userInputName) 和 pstmt.setInt(2, status)
- 错误写法:"SELECT * FROM " + tableName + " WHERE id = ?" —— 表名不能用 ? 占位,拼接即失效
- 数字、时间、布尔等类型也不能例外,必须用 setInt()、setTimestamp()、setBoolean(),避免隐式转换漏洞
禁止在 SQL 字符串中拼接任何用户可控内容
哪怕只加一个单引号、一个空格、一个字段名,都会让预编译形同虚设。
- 动态表名、列名、ORDER BY 字段、IN 子句占位符数量——这些都属于 SQL 结构,不能由用户决定;必须用白名单校验或枚举控制(如 if ("name".equals(sortField)) orderSql = "ORDER BY name")
- 不要用 String.format()、+ 连接、replaceAll() 等方式构造 SQL 字符串后再传给 prepareStatement()
- 批量 IN 查询别靠字符串拼出 "? , ? , ?",应限制最大长度(如最多 100 个),再用循环 addBatch()
复用 PreparedStatement 实例才能提升效率
预编译的优势分两层:数据库端缓存执行计划 + 客户端复用对象。如果每次查询都 new 一个 PreparedStatement,等于只用了“半截功能”。
- 在循环插入/更新时,创建一次 PreparedStatement,反复调用 setXxx() → addBatch() → executeBatch()
- 避免在方法内临时创建、用完就丢;可考虑将常用语句封装为工具类成员变量(注意线程安全)
- 连接池环境下,频繁新建 PreparedStatement 可能导致 Statement 泄漏,增加 GC 压力
配合其他措施形成完整防护
PreparedStatement 是核心防线,但不是唯一防线。
- 对非参数位置(如表名、排序字段、分页参数)做严格白名单校验
- 密码等敏感字段不走明文比对,应使用 BCrypt 加盐哈希验证
- 开启数据库的预编译开关(如 MySQL 的 useServerPrepStmts=true 和 cachePrepStmts=true)
- ORM 框架(如 MyBatis、JPA)也要确认其生成 SQL 是否参数化,警惕 ${} 与 #{} 混用
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











