preparedstatement彻底防sql注入的关键是所有用户输入必须严格通过?占位符绑定,禁止任何字符串拼接;表名、字段名等结构内容须白名单校验,in子句占位符数量按实际参数生成且逻辑不可受控,类型绑定须精确匹配,mybatis禁用${}而用#{}。

用 PreparedStatement 彻底封杀 SQL 注入,关键不是“用了没”,而是“所有用户输入是否严格走占位符绑定”——只要做到这点,数据库根本不会把输入当代码解析,注入在底层就被截断了。
只许用 ? 占位,绝不拼字符串
SQL 模板里不能出现任何变量名、用户输入或字符串拼接。哪怕一个单引号、一个加号、一个 toString() 都会破防。
- ✅ 正确:
SELECT * FROM users WHERE email = ? AND status = ? - ❌ 错误:
"SELECT * FROM " + table + " WHERE name = '" + name + "'"(表名+拼接+引号全踩雷) - ❌ 错误:
"WHERE id IN (" + ids.stream().map(x -> "?").collect(joining(",")) + ")"(看似动态占位,但若ids来自用户且未校验长度,仍可能触发资源耗尽或逻辑绕过)
所有参数必须显式绑定,类型要对得上
PreparedStatement 不吃“差不多”,setString 绑 int、setInt 绑 null、漏设第 3 个参数……都会导致异常或隐性失效。
- 用
ps.setString(1, userInput),不用ps.setObject(1, userInput)(部分驱动对 setObject 处理不一,有反序列化风险) - 时间字段用
setTimestamp,数字用setLong或setInt,布尔用setBoolean - 空值统一用
setNull(index, Types.VARCHAR),别传 null 或 ""
结构类动态内容必须白名单或枚举
表名、字段名、ORDER BY 字段、GROUP BY、LIMIT 偏移量……这些无法用 ? 占位,一旦拼接就等于开门揖盗。
- 排序字段只允许
Arrays.asList("id", "created_at", "name")中的值 - 方向只接受
"ASC"或"DESC",写死判断,不取用户原始字符串 - IN 子句占位符数量按实际参数个数生成(如 3 个 ID 就用
WHERE id IN (?, ?, ?)),但生成逻辑本身不能掺用户输入 - MyBatis 中禁用
${},一律用#{};XML 里<if test="sortField == 'price'">ORDER BY price</if>这种硬编码才安全
验证预编译是否真生效
光写对代码不够,得确认数据库收到的是预编译指令,不是拼好的 SQL 字符串。
- 开启 MySQL JDBC 日志:
jdbc:mysql://...?logger=com.mysql.cj.log.StandardLogger&profileSQL=true - 看到
COM_STMT_PREPARE和COM_STMT_EXECUTE协议调用,说明走的是预编译 - 若日志里全是
COM_QUERY加一长串带值的 SQL,说明 prepareStatement() 被绕过或连接池/驱动配置关闭了预编译 - Druid 默认支持;HikariCP 不干预,靠驱动;MySQL Connector/J 默认
useServerPrepStmts=false(推荐),即客户端仿真预编译,既安全又高效











