PreparedStatement 是防SQL注入的底线,必须配合setXxx()传参,占位符不能用于表名、列名或ORDER BY;MyBatis中#{}安全、${}危险;动态表名需白名单校验,IN查询用<foreach>,排序字段限枚举;输入验证是兜底防线,数据库账号须最小权限。

用 PreparedStatement 替代 Statement 是底线,不是可选项
只要代码里出现 Statement + 字符串拼接,就等于在数据库门口挂了“欢迎注入”的牌子。这不是危言耸听——哪怕只有一处漏网,攻击者就能绕过所有其他防护。
关键不是“有没有用”,而是“用对没用对”:
-
PreparedStatement必须配合setXxx()方法(如setString()、setLong())传参,不能把用户输入塞进 SQL 字符串再交给prepareStatement() - 占位符
?不能出现在引号内、表名、列名或ORDER BY子句中——这些位置无法参数化,必须走白名单校验或硬编码 - 不要在 try-with-resources 外提前 close
Connection,否则PreparedStatement可能失效,导致 fallback 到拼接逻辑
MyBatis 里 #{} 和 ${} 的区别不是语法糖,是安全分水岭
很多人知道 #{} → PreparedStatement、${} → 字符串拼接,但真正在动态表名、IN 列表、排序字段上栽跟头的比比皆是。
典型错误场景和应对方式:
立即学习“Java免费学习笔记(深入)”;
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 动态表名:不能用
${tableName},必须从预设白名单中匹配,比如Map.of("user", "t_user", "order", "t_order").get(tableName) - IN 查询多个 ID:
WHERE id IN (<code>#{idList}) 会报错,正确写法是 MyBatis 的<foreach>标签生成多个?占位符,而不是拼成'1','2','3' - 排序字段:
ORDER BY ${sortField}极度危险,应限定为枚举值,如SortField.valueOf(sortField).getColumn()
输入验证不是锦上添花,而是最后一道物理防线
参数化查询防不住所有路径——比如你用 java.time.format.DateTimeFormatter 解析用户传的日期字符串,结果被注入进日志语句;或者前端绕过 JS 校验直接发请求。这时候输入验证就是兜底。
实操建议:
- 数字类型优先用
Long.parseLong()或Integer.parseInt(),捕获NumberFormatException并拒绝请求,比正则更可靠 - 用户名、邮箱等字段,用白名单正则而非黑名单过滤,例如
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ - 绝对不要对输入做“转义后拼接”,比如把
'换成''再塞进 SQL——不同数据库转义规则不一致,且容易漏掉;、--、/*等
框架自动防护 ≠ 业务层零风险
Spring Data JPA 的 @Query、Hibernate 的 HQL、MyBatis 的 select 标签,都默认启用参数绑定,但它们只管你写的那部分 SQL。真正危险的是你“以为安全”的地方:
- 自定义
NativeQuery里混用${}或字符串拼接 - 用
JdbcTemplate的query(String sql, Object... args)时,误把整个 WHERE 条件当args传进去 - 日志打印 SQL 时把原始参数一起打出来,导致敏感信息泄露,还可能被 WAF 误判为攻击流量
最常被忽略的一点:数据库账号权限。即使代码全用 PreparedStatement,如果应用账号有 DROP TABLE 权限,一个注入点仍可能造成毁灭性后果。生产环境账号应仅授予 SELECT/INSERT/UPDATE 所需的最小集合,禁用 DELETE、DROP、EXECUTE。

















