PreparedStatement 防注入的关键在于所有用户输入必须通过 setXxx() 绑定,SQL 模板严禁拼接变量;表名、列名等结构化内容须白名单校验,不可用 ? 占位。

用了 PreparedStatement 不等于防住了 SQL 注入——关键在参数是否全部走 setXxx()、SQL 模板里有没有拼接用户输入、动态结构部分(如表名、排序字段)有没有白名单校验。
所有用户输入必须通过 setXxx() 绑定,不能出现在 SQL 字符串里
这是最常踩的坑:以为调了 prepareStatement() 就安全了,结果 SQL 字符串里还藏着 " + userId + " 或 String.format("WHERE name = '%s'", name)。
- ✅ 正确:
SELECT * FROM user WHERE id = ? AND status = ?,再用ps.setLong(1, userId)和ps.setString(2, status) - ❌ 错误:
"SELECT * FROM user WHERE id = " + userId + " AND status = '" + status + "'",哪怕后面套了prepareStatement(),也完全失效 - ⚠️ 特别注意:
setObject()不能代替类型明确的setString()/setInt()——传错类型可能触发隐式转换或全表扫描,且某些驱动对setObject(null, ...)处理不一致
表名、列名、ORDER BY 字段不能用 ? 占位,必须白名单校验
PreparedStatement 的 ? 只接受值(value),不接受标识符(identifier)。直接写 ORDER BY ? 会报 Parameter index out of range,或被当成字符串字面量处理(比如查 ORDER BY 'name' 而不是按 name 字段排序)。
- 正确做法:限定可选字段范围,用白名单判断后再拼接,例如:
if (!List.of("name", "email", "created_at").contains(sortField)) throw new IllegalArgumentException("Invalid sort field") - 排序方向(
ASC/DESC)同理,只允许枚举:if (!"ASC".equals(order) && !"DESC".equals(order)) throw ... - IN 子句不支持单个
?代多个值;需动态生成?, ?, ?占位符(数量由白名单控制),再循环setXxx()
异常信息必须屏蔽原始 SQLException.getMessage()
SQLException.getMessage() 常含数据库内部细节:MySQL 吐 Unknown column 'xxx' in 'where clause',PostgreSQL 报 relation "users_temp" does not exist——这些全是攻击者构造下一步注入的线索。
- 前端响应一律返回泛化提示,如
"操作失败,请稍后重试",不带任何技术词 - 日志中记录
e.getSQLState()(如23505表唯一约束冲突)和e.getErrorCode()(数据库特有码),它们稳定、无上下文 - 绝对禁用
e.printStackTrace()输出到可公开访问的日志文件或监控页面
Java 17 下的兼容性与实操细节
Java 17 是 LTS 版本,JDBC 驱动普遍支持,但要注意几个实际差异点:
- MySQL 8.0+ 驱动默认启用服务端预编译(
useServerPrepStmts=true),但需确认连接 URL 显式开启,否则仍是客户端模拟预编译 -
setTimestamp(int, Timestamp)在 PostgreSQL 驱动中默认带时区,在 MySQL 中可能截断毫秒——务必按字段定义选方法:setObject(1, localDateTime, JDBCType.TIMESTAMP)更可控 - 空值必须用
ps.setNull(index, Types.VARCHAR),不能传null给setString()(NPE)或setInt()(SQLException) - 资源关闭推荐 try-with-resources,Java 17 已全面支持自动关闭
Connection/PreparedStatement/ResultSet
真正难的不是写对一行 ps.setString(1, input),而是确保整个查询链路——从 HTTP 参数解析、条件组装、SQL 拼接点、到异常捕获层——没有一处把用户输入当“代码”而不是“数据”来对待。漏掉一个白名单、一次手动拼接、一条裸 getMessage(),防线就塌了。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











