@query + @param 能防 sql 注入,因 jpa 将命名参数转为 preparedstatement 占位符,使恶意输入仅作字符串值处理;混用 ${}、#{}、字符串拼接或动态表名/字段名则立即失效。

只要用对 @Query 和 @Param,JPA 本身就能挡住绝大多数 SQL 注入;但一旦混用字符串拼接或 ${} 风格的动态 SQL,防线就立刻失效。
为什么 @Query + @Param 能防注入
JPA 在底层把命名参数(如 :username)交给 Hibernate 转为 JDBC 的 PreparedStatement 占位符,数据库收到的是已预编译的语句结构 + 独立参数值,恶意内容不会被解析为 SQL 逻辑。
- 错误写法:
@Query("SELECT u FROM User u WHERE u.username = '" + username + "'")—— 字符串拼接,直接执行失败或被绕过 - 正确写法:
@Query("SELECT u FROM User u WHERE u.username = :username"),配合@Param("username") String username - 注意:即使参数值是
"admin' OR '1'='1",最终绑定的也只是字符串字面量,不会触发布尔逻辑
哪些 @Query 场景会悄悄破防
看似用了 @Query,但写法不当仍会引入风险。关键看是否引入运行时拼接逻辑。
-
@Query("SELECT * FROM user WHERE name LIKE '%${name}%'")——${name}是 SpEL 表达式,直接字符串替换,等同于拼接 -
@Query(value = "SELECT * FROM #{#entityName} WHERE id = :id", nativeQuery = true)——#{#entityName}是运行时计算的表名,无法参数化 - 动态排序:
@Query("SELECT * FROM user ORDER BY #{#sortField}")—— 字段名不能用:param占位,必须白名单校验后硬编码 - 解决办法:排序字段、表名、列名这类元数据,只能从预设枚举或白名单中取,不能由用户输入直接代入
JPA 查询构造器比 @Query 更安全
Criteria API 或 QueryDSL 这类构造器,全程不接触原始 SQL 字符串,天然免疫注入,适合复杂条件组合场景。
CriteriaBuilder cb = entityManager.getCriteriaBuilder();CriteriaQuery<user> cq = cb.createQuery(User.class);</user>Root<user> root = cq.from(User.class);</user>cq.select(root).where(cb.equal(root.get("username"), username));- 所有字段名、操作符都来自 Java 对象反射,没有字符串插槽可被利用
- 缺点:代码量大,简单查询没必要上;但涉及多条件 or/and 组合、权限过滤时,它比手写
@Query更可控
别让 DTO 校验变成摆设
哪怕用了参数化查询,如果没限制输入格式,攻击者仍可能用超长 payload 撞击数据库或触发慢查询。校验不是防注入的主力,但能堵住旁路。
- 用
@Size(max = 50)限制用户名长度,避免' OR 1=1 -- ...堆满 65535 字符绕过前端限制 -
@Pattern(regexp = "^[a-zA-Z0-9_]+$")拦截基础 SQL 关键字字符(注意:正则不能替代参数化,仅作辅助) - 敏感操作(如删除、导出)必须叠加服务层鉴权,不能只靠 DAO 层“看起来安全”
- 特别注意:
@Valid只校验 Controller 入参,对@Service内部调用的参数无效,需手动校验或使用 AOP
最危险的不是不会写参数化查询,而是写了 @Query 就以为万事大吉——只要出现任意一个 ${}、#{}、字符串拼接或反射调用表名/字段名的地方,整条链路就等于裸奔。防注入不是加一道锁,而是确保每个数据落点都走预编译通道。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











