SQL注入长期位居Web安全漏洞榜首,即使是基于Spring Boot和Spring Data JPA的项目,如果使用方式不当,同样会留下注入入口。不少团队存在一个误区,认为只要用了ORM框架就天然免疫SQL注入,这种想法并不准确。JPA的安全能力建立在参数化查询的基础上,一旦开发者绕开参数绑定去拼接字符串,漏洞就会重新出现。本文将从注入成因、安全查询写法和Specification规范查询三个层面,讲解如何在Spring Boot项目中构建可靠的数据访问防线。

一、JPA项目中SQL注入是怎么发生的
Spring Data JPA默认生成的CRUD方法、方法名派生查询(例如findByUserNameAndStatus)底层都使用PreparedStatement参数绑定,用户输入只会被当作数据传递给数据库,不会被解释成SQL片段,这类用法是安全的。真正的风险往往来自开发者手写的部分。
第一类高危写法是在拼接字符串后传入createNativeQuery或createQuery。有些开发者为了实现灵活查询,直接把请求参数拼进SQL里,例如"select * from user where name = '" + name + "'"这种形式。攻击者传入' or '1'='1之类的payload,就能让查询条件失效,甚至借助UNION语句拖出整张表的数据。
第二类风险点是排序字段的动态拼接。分页功能中,前端往往会上传排序字段名和排序方向,如果后端不校验就直接拼进order by子句,攻击者可以借助数据库报错信息或延时函数探测数据结构。需要特别注意,order by后面的字段无法通过参数绑定传入,因为数据库会把绑定参数当作字面值而不是标识符,这就必须用白名单校验来兜底。
第三类是滥用拼接组装JPQL。虽然@Query注解中写死字符串无法拼接变量,但通过字符串拼接后再执行原生SQL的代码在老项目中并不少见,这类代码风险与传统JDBC注入完全一致。
二、安全的查询写法:参数绑定与白名单校验
对于@Query注解,正确做法是始终使用命名参数或位置参数,让框架通过PreparedStatement完成绑定:
public interface UserRepository extends JpaRepository<User, Long> {
// 正确:命名参数绑定,输入只会被当作数据
@Query("select u from User u where u.userName = :name and u.status = :status")
List<User> findByNameAndStatus(@Param("name") String name,
@Param("status") Integer status);
// 正确:LIKE查询同样通过参数绑定,通配符写在参数值一侧
@Query("select u from User u where u.userName like concat('%', :keyword, '%')")
List<User> searchByKeyword(@Param("keyword") String keyword);
}
参数绑定之所以安全,是因为PreparedStatement在数据库侧先完成SQL语法树的编译,用户输入只能填充到占位符对应的数据位置,无法改变SQL结构。这是防御SQL注入最根本的机制,任何其他手段都只是补充。
对于无法参数化的部分,例如排序字段、排序方向、动态表名,必须使用白名单。写一个校验工具,把允许的字段名放进集合中,任何不在白名单内的输入直接拒绝或替换为默认值:
private static final Set<String> ALLOWED_SORT_FIELDS = Set.of("id", "userName", "createTime");
public Sort buildSort(String field, String direction) {
if (!ALLOWED_SORT_FIELDS.contains(field)) {
return Sort.by(Sort.Direction.DESC, "id"); // 非法输入回退默认值
}
Sort.Direction dir = "asc".equalsIgnoreCase(direction)
? Sort.Direction.ASC : Sort.Direction.DESC;
return Sort.by(dir, field);
}
此外,LIKE查询还有一个容易忽略的细节:如果用户输入包含%或_通配符,虽然通常不会造成注入,但会导致查询语义被篡改,例如输入%%会匹配全部记录,造成性能压力。严谨的做法是先转义通配符再绑定参数。
三、Specification规范查询:动态条件的安全实现
当查询条件需要根据用户筛选动态组合时,例如后台管理列表的多条件筛选,@Query就不够灵活了,这正是JPA Specification的用武之地。Specification基于JPA Criteria API构建,所有条件都通过CriteriaBuilder以类型安全的方式生成,天然不存在字符串拼接,是动态查询场景下防注入的最佳选择。
第一步,让Repository接口继承JpaSpecificationExecutor:
public interface OrderRepository extends JpaRepository<Order, Long>,
JpaSpecificationExecutor<Order> {
}
第二步,编写Specification构建动态条件。下面是一个订单列表筛选的例子,支持按状态、金额区间、关键字组合查询,条件为空时自动跳过:
public class OrderSpecs {
public static Specification<Order> queryCondition(OrderQuery query) {
return (root, cq, cb) -> {
// 用List收集所有条件,最后统一转数组
List<Predicate> list = new ArrayList<>();
if (query.getStatus() != null) {
list.add(cb.equal(root.get("status"), query.getStatus()));
}
if (query.getMinAmount() != null) {
list.add(cb.ge(root.get("amount"), query.getMinAmount()));
}
if (query.getKeyword() != null && !query.getKeyword().isBlank()) {
// 转义LIKE通配符,防止%%匹配全表
String escaped = query.getKeyword()
.replace("\\", "\\\\")
.replace("%", "\\%")
.replace("_", "\\_");
list.add(cb.like(root.get("orderNo"), "%" + escaped + "%", '\\'));
}
return cb.and(list.toArray(new Predicate[0]));
};
}
}
注意上面代码中LIKE条件的三个要点:通配符拼接发生在参数值层面而非SQL语句层面,cb.like内部依然走参数绑定;显式传入了转义字符'\\'告诉数据库如何解释转义;反斜杠本身也要先转义,避免用户输入\%绕过处理。这些细节组合起来,既保证了安全,也保证了查询语义的正确性。
第三步,在Service层调用并组合分页。Specification与Pageable可以无缝配合,排序依然建议走前面提到的白名单逻辑:
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
public Page<Order> listOrders(OrderQuery query, int page, int size) {
Pageable pageable = PageRequest.of(page, size,
buildSort(query.getSortField(), query.getSortDir()));
return orderRepository.findAll(OrderSpecs.queryCondition(query), pageable);
}
}
相比字符串拼接方案,Specification的优势在于:条件由类型安全的API生成,字段名写错会在编译期暴露;条件组合逻辑集中在一处,便于审查和测试;配合Hibernate的静态元模型,还可以进一步消除字符串形式的属性名。它的代价是代码略显冗长、上手成本略高,但在安全性和可维护性上收益明显。
四、项目落地检查清单
结合上面的分析,建议在代码评审和上线检查时关注以下几点:
- 全面排查
createNativeQuery和createQuery调用,确认没有字符串拼接用户输入,一律改为命名参数绑定。 - 排序字段、排序方向、动态表名一律走白名单校验,禁止直接透传前端参数。
- 动态多条件查询统一采用Specification实现,团队内沉淀通用的条件构建工具类。
- LIKE查询统一转义
%、_和反斜杠,并显式指定转义字符。 - 数据库账号遵循最小权限原则,应用账号不授予DDL权限,即使发生注入也能缩小影响面。
- 把SQL注入检测纳入CI流程的静态扫描,尽早发现高危写法。
防御SQL注入没有银弹,核心原则始终是数据与代码分离。在Spring Boot加Spring Data JPA的技术栈下,参数绑定负责静态查询,Specification负责动态查询,白名单负责无法参数化的标识符,三者各司其职,就能把数据访问层的安全水位提升到一个可靠的高度。
SQL注入防御Spring Data JPASpecification修改时间:2026-09-15 01:41:03