导读:本期聚焦于高永康创作的《Spring Boot项目如何防御SQL注入?使用Spring Data JPA规范查询的正确姿势》,敬请观看详情。为什么明明用了JPA还是会被扫出SQL注入漏洞?问题的根源在于部分开发者绕开了参数绑定,直接在JPQL或原生SQL里拼接字符串,导致用户输入被数据库当作SQL片段执行。本文围绕Spring Boot项目中的SQL注入防御展开,先分析注入发生的典型场景和底层原理,再对比字符串拼接与参数绑定的差异,随后重点讲解Specification规范查询的完整实现,包括JpaSpecificationExecutor接口接入、CriteriaBuilder构造动态条件、LIKE通配符转义等细节,最后给出一份可落地的项目检查清单,帮助你系统性地补齐数据访问层的安全防线,让动态多条件查询既灵活又安全。

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

Spring Boot项目如何防御SQL注入?使用Spring Data JPA规范查询的正确姿势

一、JPA项目中SQL注入是怎么发生的

Spring Data JPA默认生成的CRUD方法、方法名派生查询(例如findByUserNameAndStatus)底层都使用PreparedStatement参数绑定,用户输入只会被当作数据传递给数据库,不会被解释成SQL片段,这类用法是安全的。真正的风险往往来自开发者手写的部分。

第一类高危写法是在拼接字符串后传入createNativeQuerycreateQuery。有些开发者为了实现灵活查询,直接把请求参数拼进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的静态元模型,还可以进一步消除字符串形式的属性名。它的代价是代码略显冗长、上手成本略高,但在安全性和可维护性上收益明显。

四、项目落地检查清单

结合上面的分析,建议在代码评审和上线检查时关注以下几点:

  • 全面排查createNativeQuerycreateQuery调用,确认没有字符串拼接用户输入,一律改为命名参数绑定。
  • 排序字段、排序方向、动态表名一律走白名单校验,禁止直接透传前端参数。
  • 动态多条件查询统一采用Specification实现,团队内沉淀通用的条件构建工具类。
  • LIKE查询统一转义%_和反斜杠,并显式指定转义字符。
  • 数据库账号遵循最小权限原则,应用账号不授予DDL权限,即使发生注入也能缩小影响面。
  • 把SQL注入检测纳入CI流程的静态扫描,尽早发现高危写法。

防御SQL注入没有银弹,核心原则始终是数据与代码分离。在Spring Boot加Spring Data JPA的技术栈下,参数绑定负责静态查询,Specification负责动态查询,白名单负责无法参数化的标识符,三者各司其职,就能把数据访问层的安全水位提升到一个可靠的高度。

SQL注入防御Spring Data JPASpecification修改时间:2026-09-15 01:41:03

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260915/56962.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。