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

禁止拼接SQL字符串,必须用PreparedStatement
Java里最常见也最危险的写法就是把用户输入直接拼进String再交给Statement.execute()执行。比如"SELECT * FROM user WHERE name = '" + name + "'"——只要name是"admin' -- ",整个查询逻辑就崩了。
正确做法是用PreparedStatement,所有外部输入都走setString()等参数绑定方法:
String sql = "SELECT * FROM user WHERE name = ? AND status = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, userInputName); // 自动转义单引号、分号等 ps.setInt(2, statusValue);
注意:PreparedStatement不是“用了就安全”,它只对?占位符位置的参数做预编译处理;如果SQL模板本身含拼接(比如动态表名、字段名),照样会出问题。
ORM框架不等于免疫SQL注入
很多人以为用了MyBatis或JPA就高枕无忧,但MyBatis的${}语法和JPA的@Query(value = "...", nativeQuery = true)仍允许字符串拼接。例如MyBatis中写WHERE name = '${name}',跟手拼SQL没区别。
应严格遵循:
- MyBatis只用
#{}传参,禁用${},除非你100%控制变量值(如枚举固定的表名) - JPA自定义查询优先用方法命名(
findByUsernameAndStatus()),避免nativeQuery;若必须用,参数一律走:param绑定 - Hibernate的
createSQLQuery()必须配合setParameter(),不能用addScalar()后直接拼字符串
存储过程里也不能拼SQL
有些团队把SQL逻辑下推到数据库,以为“代码在DB里就安全了”。错。SQL Server的EXEC(@sql)、MySQL的PREPARE+EXECUTE,只要@sql里含用户输入,照样可注入。
将后端 Java 接口文档转换为前端 TypeScript 类型定义(interface / type)。输入:飞书文档、Spring Boot 接口文档、Swagger/OpenAPI、手写 API 文档等。输出:包含请求参数与响应结果的 .md 文件,含完整 JSDoc 注释。触发关键词:Java 转 TS、接口转换、生成前端类型、接口文档转 TypeScript、DTO/VO/BO 转 interface、后端文档转前端类型、生成接口类型、类型定义生成。
可行方案只有两种:
- 用
sp_executesql(SQL Server)或EXECUTE IMMEDIATE ... USING(Oracle/PostgreSQL),且所有变量必须作为参数传入 - 彻底放弃动态拼接,改用条件分支(
CASE WHEN)、白名单字段映射或应用层路由
特别注意:MySQL中CONCAT()拼接用户输入生成WHERE条件,也是典型漏洞点。
连日志和错误信息都不能暴露SQL结构
开发时习惯把SQLException.getMessage()直接打日志甚至返回前端,结果报错里带出WHERE id = '1'' OR '1'='1'这种片段,等于帮攻击者验证注入点。
必须做到:
- 捕获
SQLException后,只记录错误类型(如"SQLSyntaxErrorException")和操作上下文(如"queryUserById"),绝不记录原始SQL或堆栈中含SQL的行 - 前端响应统一返回
500 Internal Error,不泄露数据库驱动、版本、表名等指纹信息 - 启用数据库审计日志时,确认其不记录
bind variables值(否则敏感数据反而出现在DB日志里)
真正难防的从来不是技术手段,而是开发时那一秒的“我就临时拼一下,肯定没问题”的侥幸——而攻击者只需要抓住一次。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










