派生方法和@query命名参数默认安全,因hibernate底层生成preparedstatement,参数全程绑定(如setstring()),sql结构固定、用户输入仅入占位符,不参与字符串拼接;集合参数自动展开为等长?占位符,specification动态条件也保持预编译。

派生方法和@Query命名参数为什么默认安全
Spring Data JPA 的 findByUsername、findByStatusAndRole 这类派生方法,以及 @Query("SELECT u FROM User u WHERE u.email = :email") 配合 @Param 的写法,底层由 Hibernate 生成 PreparedStatement,参数走 setString()、setLong() 等绑定,SQL 结构完全固定,用户输入只进参数槽位,不参与 SQL 字符串拼接。
- 集合参数如
@Query("... WHERE u.id IN :ids")+@Param("ids") List<long></long>,Hibernate 会自动展开成对应数量的?占位符,不是字符串替换 -
JpaRepository自带的save()、findById()、findAll(Specification)全部不暴露 SQL 构造过程,也天然免疫 - 只要没在 JPQL 或原生 SQL 字符串里用
+或String.format插入变量,就不用额外加防注入逻辑
@Query(nativeQuery = true) 是怎么破防的
原生 SQL 绕过 Hibernate 解析层,直接交由 JDBC 执行。它只保证 :param 或 ?1 这种占位符走预编译,但整个 SQL 字符串由开发者负责构造——一旦混入用户输入,防线立即失效。
- 错误写法:
@Query(value = "SELECT * FROM user WHERE name = '" + name + "'", nativeQuery = true),字符串拼接直接触发注入 - SpEL 表达式更隐蔽:
@Query(value = "SELECT * FROM user WHERE name = '#{#name}'", nativeQuery = true),#{}是运行时字符串替换,等同于拼接 -
IN子句不能写WHERE id IN :ids(JPA 不支持原生 SQL 中数组参数展开),必须提前确定长度,例如WHERE id IN (?1, ?2, ?3)再传三个值 - 动态表名、列名、排序方向(
ASC/DESC)无法参数化,@Query里写FROM :table会报错或查不到数据,不是安全用法
动态条件该用 Specification 而不是 StringBuilder 拼 SQL
业务常要“按需加 WHERE”,有人用 StringBuilder 拼完 JPQL 再塞进 @Query,这是典型破防:因为 @Query 是编译期固定的,运行时无法改变结构,所谓“动态”只能靠外部字符串拼接,而拼接即注入温床。
- 正确做法是实现
Specification,比如cb.like(root.get("name"), "%" + name + "%"),其中name是纯参数值,%写死在代码里,不来自用户输入 - 嵌套条件、多表关联、复杂
OR/AND分组,都可用Specifications.where().and().or()安全组合 - 每个条件封装为独立谓词,可复用、可测试、类型安全,最终仍生成预编译语句
- 避免在 service 层偷偷用
String.format拼完 SQL 再丢给JdbcTemplate——这种代码不报错,但等于裸奔
真需要动态表名或字段时怎么办
当查询涉及多租户分表、按业务类型切换主表、实时字段聚合等场景,@Query 已不是合适工具。硬塞只会让校验逻辑越来越重,出错概率越来越高。
- 改用
JdbcTemplate+ 白名单校验 +PreparedStatement:先调用validateTableName(inputTable),只允许"user_2026"、"order_v2"等预设值,不匹配就抛IllegalArgumentException - 排序字段白名单示例:
Set.of("created_at", "updated_at", "score"),禁止接收request.getParameter("sort")直接进 SQL - 动态
IN列表可用StringBuilder构建固定长度占位符,再用PreparedStatement.setLong(i, id)逐个绑定,而非拼字符串 - 最麻烦的地方往往不在参数怎么写,而在于团队有人悄悄在 service 层用
String.format拼 SQL —— 这类代码难审计、难拦截,必须靠 Code Review 和静态扫描工具卡住











