首页 / 帮助文档 / Spring Data JPA查询防注入

Spring Data JPA查询防注入

Spring Data JPA 的查询防注入,核心问题不在于 JPA 本身不安全,而在于开发者经常在它提供的安全抽象之上,亲手打开后门。很多人以为用了 JPA 就不用管 SQL 注入了,结果在拼接 JPQL 或者滥用原生查询时,把参数直接嵌进了查询字符串,漏洞就这么产生了。真正要防的,是你自己绕过框架安全机制的那些操作。

JPA 的安全底线在哪里

JPA 的 EntityManager 和 Spring Data 的衍生查询方法,底层都是用参数化查询实现的。比如你写一个 findByUsername(String username),Spring Data 会自动把它转成 WHERE username = ?,然后通过 PreparedStatement 绑定参数。这个过程中,参数值永远不会被当作 SQL 指令的一部分来解析,所以注入无从发生。问题出在当你手动写 JPQL 或者 SQL 的时候,用了字符串拼接而不是参数绑定,等于把安全机制短路了。

JPQL 拼接是最常见的坑

很多项目里能看到这样的代码:

String jpql = "SELECT u FROM User u WHERE u.username = '" + username + "'";
Query query = entityManager.createQuery(jpql);

这跟当年用 JDBC 拼字符串没有任何区别。攻击者输入 admin' OR '1'='1,查询逻辑就被篡改了。正确做法是使用命名参数或者位置参数:

String jpql = "SELECT u FROM User u WHERE u.username = :username";
Query query = entityManager.createQuery(jpql);
query.setParameter("username", username);

JPA 的参数绑定机制会确保传入的值被正确转义,从根本上杜绝注入。位置参数用 ?1、?2 同理,但命名参数可读性更好,推荐优先使用。

原生查询是重灾区

当你不得不写原生 SQL 时,风险成倍增加。EntityManager.createNativeQuery 同样支持参数绑定,但很多开发者图省事,直接拼字符串。更隐蔽的是,有些人在原生查询里用占位符,却用 String.format 把参数填进去,这等于没做任何防护。原生查询的参数绑定写法如下:

String sql = "SELECT * FROM users WHERE username = :username";
Query query = entityManager.createNativeQuery(sql, User.class);
query.setParameter("username", username);

注意,这里用的是 JPA 的命名参数语法,而不是 JDBC 的问号占位符。JPA 会负责把命名参数转换成底层 PreparedStatement 的占位符,安全性和便利性兼得。

Spring Data JPA 的 @Query 注解同样需要警惕

在 Repository 接口里用 @Query 定义查询很常见。只要你用冒号参数的形式,就是安全的:

@Query("SELECT u FROM User u WHERE u.username = :username")
User findByUsername(@Param("username") String username);

但如果你在 @Query 里用 SpEL 表达式或者字符串拼接动态构建查询,就要小心了。尤其是结合原生查询时,有些开发者会用 ? 占位符但忘记 Spring Data 默认是按位置绑定的,导致参数错位或直接拼接。记住一个原则:永远不要在查询字符串里看到加号连接参数值。

动态查询的防注入策略

复杂业务场景下,查询条件往往是动态组合的。这时候很多人会掉进拼接的陷阱。安全的做法有三种。第一种是用 Criteria API,它是 JPA 提供的类型安全的动态查询构建器:

CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery cq = cb.createQuery(User.class);
Root root = cq.from(User.class);
List predicates = new ArrayList<>();
if (username != null) {
    predicates.add(cb.equal(root.get("username"), username));
}
cq.where(predicates.toArray(new Predicate[0]));

Criteria API 的所有条件都是通过方法调用构建的,参数自动绑定,完全不存在字符串拼接,因此天然防注入。缺点是代码冗长,可读性稍差。

第二种是 Spring Data 的 Specification 接口,它本质上是 Criteria API 的封装,写起来更符合 Spring 风格:

Specification spec = (root, query, cb) -> {
    List predicates = new ArrayList<>();
    if (username != null) {
        predicates.add(cb.equal(root.get("username"), username));
    }
    return cb.and(predicates.toArray(new Predicate[0]));
};
List users = userRepository.findAll(spec);

第三种是 Querydsl,它提供了一套流畅的 API 来构建类型安全的查询,编译期就能检查字段名是否正确,安全性同样有保障。无论选哪种,核心思想都是把查询结构的构建和参数值的绑定分开,让框架处理参数转义。

LIKE 查询的注入风险

LIKE 模糊查询容易被忽视。即使你用了参数绑定,如果不对用户输入的通配符做处理,攻击者可以注入 % 或 _ 来改变查询意图,比如让一个本应精确匹配的查询变成全表扫描,造成性能攻击。更危险的是,有些数据库允许在 LIKE 模式里嵌入逻辑条件,虽然现代数据库对此已有防护,但仍需谨慎。建议在应用层对用户输入的通配符进行转义:

String escaped = username.replace("\\", "\\\\")
                         .replace("%", "\\%")
                         .replace("_", "\\_");
query.setParameter("username", "%" + escaped + "%");

同时在 JPQL 里使用 ESCAPE 子句指定转义字符,确保数据库也按你的预期处理。

IN 子句的参数绑定陷阱

IN 查询需要传入一个集合,很多开发者会犯这样的错误:

String ids = "1,2,3";
String jpql = "SELECT u FROM User u WHERE u.id IN (" + ids + ")";

这等于把参数直接拼接进去了。正确做法是利用 JPA 的参数绑定支持集合类型:

List idList = Arrays.asList(1L, 2L, 3L);
String jpql = "SELECT u FROM User u WHERE u.id IN :ids";
query.setParameter("ids", idList);

JPA 会自动展开集合,生成正确的 IN 子句并绑定每个参数。这个特性很多开发者不知道,导致写出有漏洞的代码。

排序字段和表名的注入防护

参数绑定只能保护值类型的参数,对于排序字段名、排序方向、表名这类标识符,JPA 无法通过参数绑定处理。如果你的接口允许前端传排序字段,必须做白名单校验:

private static final Set ALLOWED_SORT_FIELDS = Set.of("id", "username", "email", "createTime");

public List findUsersByOrder(String sortField) {
    if (sortField == null || !ALLOWED_SORT_FIELDS.contains(sortField)) {
        throw new IllegalArgumentException("Invalid sort field");
    }
    // 此时可以安全地拼接到 JPQL 或 Criteria 中
}

绝对不要直接把用户输入的字段名拼进查询字符串。表名同理,如果确实需要动态表名,必须用白名单或者映射表来控制,没有例外。

存储过程调用的安全考量

JPA 调用存储过程时,参数绑定同样适用。用 StoredProcedureQuery 注册参数并设置值,不要自己拼 CALL 语句。存储过程内部也要遵循参数化原则,避免在过程体内拼接动态 SQL,否则即使 JPA 层面安全,数据库层面仍然有注入风险。

ORM 框架不是银弹

JPA 和 Spring Data 提供了强大的安全基础,但前提是你得按规矩用。任何绕过参数绑定机制的写法,都是在给自己挖坑。代码审查时,看到 createQuery 或 createNativeQuery 里的字符串拼接,就应该直接标记为阻塞项。同时,静态代码分析工具可以辅助检测这类模式,但最终责任在开发者对安全机制的理解和遵守。

防注入的本质不是记住某个框架的特定写法,而是理解参数化查询的原理,并始终把用户输入当作不可信数据来对待。只要你的查询字符串里永远不直接包含用户输入的值,注入就无从下手。JPA 给了你所有需要的工具,剩下的就是别自己把门打开。