过滤器

在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的基础类; 与SpringApplicationContext生命周期集成。spring-doc.cadn.net.cn

  • OncePerRequestFilter — 作为GenericFilterBean的扩展,支持在请求开始时进行单次调用,即在REQUEST分发阶段,并且忽略通过FORWARD分发的进一步处理。此过滤器还提供了控制Filter是否参与ASYNCERROR分发的选项。spring-doc.cadn.net.cn

Servlet 过滤器可以通过 web.xml 配置或使用 Servlet 注解配置。 在 Spring Boot 应用中,你可以 将过滤器声明为 Bean ,Spring 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

The 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-PrefixX-Forwarded-Forspring-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-端口

尽管不是标准,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)是一个实际中广泛使用的头部,用于向下游服务器传达原始协议(例如,https / http)。例如,如果将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

X-转发-For

X-Forwarded-For: <address> 通常是用来通信客户端原始InetSocketAddress到下游服务器的事实标准头部。例如,如果一个请求由位于[fd00:fefe:1::4]的客户端发送到位于192.168.0.1的代理,HTTP请求中包含的“远程地址”信息将反映客户端的实际地址,而不是代理的地址。spring-doc.cadn.net.cn

ForwardedHeaderFilter

ForwardedHeaderFilter 是一个Servlet过滤器,用于修改请求以 a) 根据 Forwarded 头信息更改主机、端口和协议,并 b) 移除这些头信息以消除进一步的影响。该过滤器依赖于包装请求,并且必须在其他过滤器之前进行排序,例如 RequestContextFilter,以便它们可以使用修改后而不是原始的请求。spring-doc.cadn.net.cn

安全注意事项

请求头转发存在安全考虑,因为应用程序无法知道这些标头是被代理按预期添加的,还是被恶意客户端添加的。这就是为什么在信任边界的代理应该被配置为移除来自外部的不可信Forwarded标头。你还可以配置ForwardedHeaderFilter使用removeOnly=true,在这种情况下,它会移除但不使用这些标头。spring-doc.cadn.net.cn

分派器类型

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

浅层ETag

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

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

此过滤器有一个 writeWeakETag 参数,用于配置过滤器以写入类似于以下的弱 ETags: 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/”都应由带有@RequestMapping注解的控制器方法处理。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(无路径属性)。spring-doc.cadn.net.cn