@query注解中禁止字符串拼接用户输入,命名参数仅安全用于值绑定;动态表名/列名须白名单校验后硬编码;integer等强类型参数+like固定通配符+in枚举白名单组合可防注入。

Query注解里用字符串拼接就是SQL注入温床
直接在 @Query 的 JPQL 或原生 SQL 字符串里用 + 拼接用户输入,等同于把数据库钥匙塞进请求参数里。JPQL 不支持预编译占位符(如 ?1)与动态字段名/表名混用,但命名参数(:name)本身是安全的——前提是不用它去拼接 SQL 结构。
命名参数只能绑定值,不能绑定列名或表名
这是最容易踩的坑:以为写了 @Query("SELECT * FROM :table WHERE name = :name") 就算用了命名参数,其实 :table 会被当成字面量字符串处理,不是参数替换,运行时直接报错或查不到数据。JPA 规范明确禁止运行时解析动态表名/列名。
-
:param只能出现在 WHERE、HAVING、ORDER BY(值部分)、GROUP BY(值部分)等**值上下文**中 - 表名、列名、函数名、排序方向(ASC/DESC)必须硬编码,或通过外部校验后白名单拼接
- 若真需动态表名,改用
JdbcTemplate+ 白名单校验 +PreparedStatement,别硬塞进@Query
安全写法:命名参数 + 严格类型约束 + 白名单兜底
下面这个例子看起来普通,但每一步都在防注入:
@Query("SELECT u FROM User u WHERE u.status = :status AND u.name LIKE %:name%")
List<user> findUsers(@Param("status") Integer status, @Param("name") String name);
</user>
-
status是Integer类型,JPA 会自动转成 JDBCsetInt(),不可能触发字符串逃逸 -
name虽为String,但只用于LIKE右侧的模糊匹配,且%写死在 SQL 里,不是从参数拼进来的 - 如果业务要求按多个状态筛选,别写
"status IN :statuses"(JPA 不支持数组参数直接展开),改用IN动态构建 + 白名单校验后的枚举值列表
原生 SQL 下命名参数仍安全,但得避开 CONCAT 和字符串函数
用 @Query(nativeQuery = true) 时,命名参数依然走 JDBC 预编译,但要注意某些数据库函数会绕过参数化机制:
- 避免
CONCAT(:a, :b)或:col = :val这种把参数当标识符用的写法 - PostgreSQL 的
quote_ident(:table)看似安全,但 JPA 不识别该函数,传进去的仍是未转义字符串 - 真正需要动态字段时,先用
StringUtils.replaceChars(fieldName, ".", "_")做基础清洗,再比对预设白名单(如Set.of("user_name", "created_at")),不匹配就抛IllegalArgumentException
最麻烦的地方往往不在参数怎么写,而在于团队有人偷偷在 service 层用 String.format 拼完 SQL 再丢给 JdbcTemplate——这种代码不会报编译错误,但只要参数含单引号或分号,就等于敞开大门。盯紧日志里的 SQL 打印,比什么都管用。











