对于最新稳定版本,请使用 Spring Framework 7.0.6spring-doc.cadn.net.cn

过滤器

在 Servlet API 中,你可以添加一个 jakarta.servlet.Filter,以在过滤器处理链和目标 Servlet 的其余处理逻辑之前和之后应用拦截式逻辑。spring-doc.cadn.net.cn

spring-web 模块包含多个内置的 Filter 实现:spring-doc.cadn.net.cn

Spring 应用程序中也有可供使用的基类实现:spring-doc.cadn.net.cn

  • GenericFilterBean — 作为 Spring Bean 配置的 Filter 的基类; 与 Spring ApplicationContext 生命周期集成。spring-doc.cadn.net.cn

  • OncePerRequestFilter — GenericFilterBean 的扩展类,支持在请求开始时(即 REQUEST 分发阶段)仅调用一次,并忽略通过 FORWARD 分发的后续处理。该过滤器还提供控制选项,决定是否在 FilterASYNC 分发中启用该 ERRORspring-doc.cadn.net.cn

Servlet 过滤器可以在 web.xml 中配置,也可以通过 Servlet 注解进行配置。 在 Spring Boot 应用程序中,您可以 将 Filter 声明为 Bean, Boot 将自动完成它们的配置。spring-doc.cadn.net.cn

表单数据

浏览器只能通过 HTTP GET 或 HTTP POST 提交表单数据,但非浏览器客户端还可以使用 HTTP PUT、PATCH 和 DELETE。Servlet API 要求 ServletRequest.getParameter*() 方法仅对 HTTP POST 支持表单字段的访问。spring-doc.cadn.net.cn

spring-web 模块提供了 FormContentFilter,用于拦截内容类型为 application/x-www-form-urlencoded 的 HTTP PUT、PATCH 和 DELETE 请求,从请求体中读取表单数据,并包装 ServletRequest,使得可以通过 ServletRequest.getParameter*() 系列方法获取表单数据。spring-doc.cadn.net.cn

转发的请求头

当请求经过负载均衡器等代理时,主机、端口和协议方案可能会发生变化,这使得从客户端视角创建指向正确主机、端口和协议方案的链接变得具有挑战性。spring-doc.cadn.net.cn

RFC 7239 定义了 Forwarded HTTP 请求头, 代理可以使用该请求头来提供有关原始请求的信息。spring-doc.cadn.net.cn

非标准头信息

还有其他一些非标准的头部字段,包括 X-Forwarded-HostX-Forwarded-PortX-Forwarded-ProtoX-Forwarded-SslX-Forwarded-Prefixspring-doc.cadn.net.cn

X-Forwarded-Host

虽然并非标准,但 X-Forwarded-Host: <host> 是一个事实上的标准请求头,用于将原始主机信息传达给下游服务器。例如,如果将针对 example.com/resource 的请求发送到代理,而该代理将请求转发到 localhost:8080/resource,则可以发送一个 X-Forwarded-Host: example.com 请求头,以告知服务器原始主机为 example.comspring-doc.cadn.net.cn

X-Forwarded-Port

虽然不是标准规范,但 X-Forwarded-Port: <port> 是一种事实上的标准请求头,用于将原始端口信息传递给下游服务器。例如,如果一个对 example.com/resource 的请求被发送到某个代理服务器,该代理服务器再将请求转发至 localhost:8080/resource,那么可以附带一个 X-Forwarded-Port: 443 请求头,以告知目标服务器原始请求的端口是 443spring-doc.cadn.net.cn

X-Forwarded-Proto

虽然这不是标准,但 X-Forwarded-Proto: (https|http) 是一个事实上的标准头部,用于向下游服务器传达原始协议(例如,https / http)。例如,如果将 example.com/resource 的请求发送到 代理,该代理将请求转发到 localhost:8080/resource,则可以发送一个 X-Forwarded-Proto: https 的头部,以通知服务器原始协议是 httpsspring-doc.cadn.net.cn

X-Forwarded-Ssl

虽然不是标准规范,但 X-Forwarded-Ssl: (on|off) 是一个事实上的标准请求头,用于向下游服务器传递原始协议(例如 http / https)。例如,如果对 example.com/resource 的请求被发送到一个代理服务器,该代理将请求转发至 localhost:8080/resource,那么代理会添加 X-Forwarded-Ssl: on 请求头,以告知后端服务器原始协议为 httpsspring-doc.cadn.net.cn

X-Forwarded-Prefix

虽然并非标准,但 X-Forwarded-Prefix: <prefix> 是一个事实上的标准请求头,用于将原始 URL 路径前缀传达给下游服务器。spring-doc.cadn.net.cn

X-Forwarded-Prefix 的使用会因部署场景而异,需要具备灵活性,以便替换、移除或在目标服务器的路径前缀前添加内容。spring-doc.cadn.net.cn

场景1:覆盖路径前缀spring-doc.cadn.net.cn

https://example.com/api/{path} -> http://localhost:8080/app1/{path}

前缀是捕获组 {path} 之前路径的起始部分。对于代理来说,前缀是 /api,而对于服务器来说,前缀是 /app1。在这种情况下,代理可以发送 X-Forwarded-Prefix: /api,以使原始前缀 /api 覆盖服务器前缀 /app1spring-doc.cadn.net.cn

场景2:移除路径前缀spring-doc.cadn.net.cn

有时,应用程序可能希望移除该前缀。例如,考虑以下代理到服务器的映射:spring-doc.cadn.net.cn

https://app1.example.com/{path} -> http://localhost:8080/app1/{path}
https://app2.example.com/{path} -> http://localhost:8080/app2/{path}

代理没有前缀,而应用程序 app1app2 分别具有路径前缀 /app1/app2。代理可以发送 X-Forwarded-Prefix: , 以使用空前缀覆盖服务器上的前缀 /app1/app2spring-doc.cadn.net.cn

这种部署场景的一个常见情况是,许可证按生产应用服务器收费,因此更倾向于在每台服务器上部署多个应用程序以降低费用。另一个原因是在同一台服务器上运行更多应用程序,以便共享服务器运行所需的资源。spring-doc.cadn.net.cn

在这些场景中,应用程序需要一个非空的上下文根路径,因为同一服务器上部署了多个应用程序。然而,在公共 API 的 URL 路径中不应显示该上下文根路径,因为应用程序可能会使用不同的子域名,这样可以带来如下好处:spring-doc.cadn.net.cn

场景3:插入路径前缀spring-doc.cadn.net.cn

在其他情况下,可能需要添加前缀。例如,考虑以下代理到服务器的映射:spring-doc.cadn.net.cn

https://example.com/api/app1/{path} -> http://localhost:8080/app1/{path}

在这种情况下,代理的前缀为 /api/app1,而服务器的前缀为 /app1。代理可以发送 X-Forwarded-Prefix: /api/app1,以使原始前缀 /api/app1 覆盖服务器前缀 /app1spring-doc.cadn.net.cn

ForwardedHeaderFilter

ForwardedHeaderFilter 是一个 Servlet 过滤器,用于修改请求,以实现以下两个目的: a) 根据 Forwarded 头信息更改主机、端口和协议(scheme),以及 b) 移除这些头信息,以避免后续产生影响。该过滤器通过包装请求来实现功能,因此必须排在其他过滤器(例如 RequestContextFilter)之前,以确保这些过滤器能够处理修改后的请求,而不是原始请求。spring-doc.cadn.net.cn

安全注意事项

由于应用程序无法判断转发头(forwarded headers)是由代理按预期添加的,还是由恶意客户端伪造的,因此在使用转发头时存在安全方面的考量。这就是为什么应在信任边界的代理上进行配置,以移除来自外部的不可信 Forwarded 头信息。你也可以将 ForwardedHeaderFilter 配置为 removeOnly=true,在这种情况下,该过滤器会移除这些头信息但不会使用它们。spring-doc.cadn.net.cn

调度器类型

为了支持异步请求和错误分发,此过滤器应映射到DispatcherType.ASYNC以及DispatcherType.ERROR。 如果使用 Spring Framework 的AbstractAnnotationConfigDispatcherServletInitializer (请参阅Servlet 配置),所有过滤器将自动为所有分发类型注册。但是,如果通过web.xml注册过滤器,或在 Spring Boot 中通过FilterRegistrationBean注册,请确保除了DispatcherType.REQUEST之外,还包含DispatcherType.ASYNCDispatcherType.ERRORspring-doc.cadn.net.cn

浅层ETag

ShallowEtagHeaderFilter 过滤器通过缓存写入响应的内容并从中计算 MD5 哈希值来创建一个“浅层”ETag。下一次客户端发送请求时,它会执行相同的操作,但还会将计算出的值与 If-None-Match 请求头进行比较,如果两者相等,则返回 304(NOT_MODIFIED)。spring-doc.cadn.net.cn

该策略节省了网络带宽,但并未节省 CPU 资源,因为每次请求都必须完整计算响应内容。 状态变更的 HTTP 方法以及其他 HTTP 条件请求头(例如 If-MatchIf-Unmodified-Since)不在本过滤器的处理范围内。在控制器层面采用其他策略 可以避免进行完整计算,并对 HTTP 条件请求提供更广泛的支持。 参见 HTTP 缓存spring-doc.cadn.net.cn

该过滤器具有一个 writeWeakETag 参数,用于配置过滤器以写入弱 ETag, 类似于以下格式:W/"02a2d595e6ed9a0b24f027f2b63b134d6"(如 RFC 7232 第 2.3 节 中所定义)。spring-doc.cadn.net.cn

为了支持异步请求,此过滤器必须使用DispatcherType.ASYNC进行映射,以便过滤器能够延迟并成功生成 ETag 直到最后一次异步调度结束。如果使用 Spring Framework 的AbstractAnnotationConfigDispatcherServletInitializer(请参阅Servlet 配置),所有过滤器将自动为所有调度类型注册。但是,如果通过web.xml注册过滤器,或在 Spring Boot 中通过FilterRegistrationBean注册,请务必包含DispatcherType.ASYNCspring-doc.cadn.net.cn

CORS(跨域资源共享)

Spring MVC 通过控制器上的注解提供了细粒度的 CORS 配置支持。然而,当与 Spring Security 一起使用时,我们建议依赖内置的 CorsFilter,该过滤器必须排在 Spring Security 过滤器链之前。spring-doc.cadn.net.cn

有关更多详细信息,请参阅CORSCORS 过滤器部分。spring-doc.cadn.net.cn

URL 处理器

你可能希望你的控制器端点能够匹配 URL 路径中带有或不带尾部斜杠的路由。 例如,"GET /home" 和 "GET /home/" 都应由使用 @GetMapping("/home") 注解的控制器方法处理。spring-doc.cadn.net.cn

Spring 提供了 UrlHandlerFilter,用于从 URL 路径中移除末尾的斜杠,以确保对带或不带末尾斜杠的路径具有一致的视图。 这对于避免基于 URL 的授权决策与 Web 框架的请求映射之间出现不匹配非常重要。 该过滤器可以通过以下几种方式之一来移除末尾的斜杠:spring-doc.cadn.net.cn

历史上,Spring MVC 支持 URL 路径的尾部斜杠匹配。 出于安全原因,该功能在 6.0 版本中已被弃用,并在 7.0 版本中移除, 由 UrlHandlerFilter 提供了一种更安全的替代方案。

以下是如何为博客应用程序实例化和配置一个UrlHandlerFilter的方法:spring-doc.cadn.net.cn

UrlHandlerFilter urlHandlerFilter = UrlHandlerFilter
		// will HTTP 308 redirect "/blog/my-blog-post/" -> "/blog/my-blog-post"
		.trailingSlashHandler("/blog/**").redirect(HttpStatus.PERMANENT_REDIRECT)
		// will wrap the request to "/admin/user/account/" and make it as "/admin/user/account"
		.trailingSlashHandler("/admin/**").wrapRequest()
		.build();
val urlHandlerFilter = UrlHandlerFilter
		// will HTTP 308 redirect "/blog/my-blog-post/" -> "/blog/my-blog-post"
		.trailingSlashHandler("/blog/**").redirect(HttpStatus.PERMANENT_REDIRECT)
		// will wrap the request to "/admin/user/account/" and make it as "/admin/user/account"
		.trailingSlashHandler("/admin/**").wrapRequest()
		.build()

请牢记以下内容:spring-doc.cadn.net.cn

  • 根路径 "/" 被排除在尾部斜杠处理之外。spring-doc.cadn.net.cn

  • @RequestMapping("/") 会在类级别的映射路径末尾添加一个斜杠,因此在启用了末尾斜杠处理时将无法匹配;请改用 @RequestMapping(不指定 path 属性)。spring-doc.cadn.net.cn