PreparedStatement 防 SQL 注入本质是参数与 SQL 语句分离,外部输入仅作数据不参与语法解析;表名、列名等非参数化位置须用白名单校验,禁用字符串拼接和手动转义。

用 PreparedStatement 替代字符串拼接
SQL 注入能跑通,本质是用户输入直接进了 SQL 语句体。最直接的阻断方式,就是不让任何外部输入参与 SQL 字符串构建。
常见错误现象:String sql = "SELECT * FROM user WHERE name = '" + request.getParameter("name") + "'"; 这种写法,只要传入 ' OR '1'='1 就全表泄露。
- Java 中必须用
PreparedStatement,参数用?占位,再调用setString()等方法赋值 - 数据库驱动会把参数当作纯数据处理,不参与语法解析,连引号都不需要手动加
- 注意:
PreparedStatement对表名、列名、排序字段(ORDER BY后)无效——这些不能参数化,需走白名单校验
对非参数化位置做严格白名单校验
当必须动态拼接表名、字段名或 ORDER BY 子句时,不能靠正则过滤单引号或分号,那只是掩耳盗铃。
使用场景:多租户路由到不同表、前端传 sort=created_at 控制排序字段。
- 维护一个明确允许的字符串集合,比如
Set.of("id", "name", "created_at", "status") - 校验时用
.contains()判断,不匹配就拒绝请求(HTTP 400),不 fallback 到默认值 - 避免用
toLowerCase()后比对——大小写敏感的数据库(如 PostgreSQL)可能绕过校验 - 别把白名单硬编码在 Controller 里,应抽到配置或枚举中,方便审计和变更
Web 层校验要早于 DAO 层,且不可绕过
很多人把校验逻辑放在 Service 或 DAO 层,但攻击者只要绕过 Web 入口(比如直调内部 RPC、伪造 HTTP 头),校验就形同虚设。
性能影响:校验本身开销极小,但若在 Filter 或 Interceptor 中做了重复解析(如多次调用 getParameterMap()),可能触发 Servlet 容器的 request body 读取异常。
- 校验必须在
HttpServletRequest解析完成后的最早时机执行,推荐用 Spring 的@Valid+ 自定义ConstraintValidator - 对关键参数(如
id、email)用类型化绑定,比如@RequestParam Long id,Spring 会自动拒掉非数字输入 - 禁用
request.getParameter()手动取值——它不触发类型转换和注解校验,等于裸奔 - 所有校验失败必须返回明确错误(如
400 Bad Request),不要吞掉异常或返回空结果
警惕“已转义就安全”的幻觉
用 StringUtils.escapeSql() 或手写 replaceAll("'", "''") 不解决根本问题,反而制造虚假安全感。
原因很简单:SQL 解析器不按你想象的方式切分字符串;不同数据库转义规则不同;而现代注入手法(如宽字节、二次编码、JSON 嵌套)能轻易绕过简单替换。
- MySQL 的
SET NAMES gbk下,%df%27可能被解析成单引号——字符集层面的漏洞,转义函数根本看不到 - PostgreSQL 支持数组、JSON 字段,
WHERE data @> '{"role":"admin"}'这类写法里,单引号在 JSON 字符串内,转义逻辑极易出错 - 如果用了 MyBatis,别迷信
${}加手动转义——${}本意就是拼接,应彻底改用#{}
真正难的不是写对一行 PreparedStatement,而是确保整个调用链上没有任何一处漏掉参数化,也没有任何一环把校验逻辑当成可选开关。











