过滤器

spring-web 模块提供了一些有用的过滤器

Servlet 过滤器可以在 web.xml 配置文件中配置,或使用 Servlet 注解。如果您使用 Spring Boot,您可以将它们声明为 Bean 并作为应用程序的一部分进行配置

表单数据

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

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

转发头

当请求通过负载均衡器等代理时,主机、端口和方案可能会发生变化,这使得从客户端的角度创建指向正确主机、端口和方案的链接成为一项挑战。

RFC 7239 定义了 Forwarded HTTP 头,代理可以使用它来提供有关原始请求的信息。

非标准头

还有其他一些非标准头,包括 X-Forwarded-HostX-Forwarded-PortX-Forwarded-ProtoX-Forwarded-SslX-Forwarded-Prefix

X-Forwarded-Host

虽然不是标准,但 X-Forwarded-Host: <host> 是一个事实标准头,用于将原始主机传递给下游服务器。例如,如果将对 example.com/resource 的请求发送到代理,代理将请求转发到 localhost:8080/resource,则可以发送 X-Forwarded-Host: example.com 头来通知服务器原始主机是 example.com

X-Forwarded-Port

虽然不是标准,但 X-Forwarded-Port: <port> 是一个事实标准头,用于将原始端口传递给下游服务器。例如,如果将对 example.com/resource 的请求发送到代理,代理将请求转发到 localhost:8080/resource,则可以发送 X-Forwarded-Port: 443 头来通知服务器原始端口是 443

X-Forwarded-Proto

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

X-Forwarded-Ssl

虽然不是标准,但 X-Forwarded-Ssl: (on|off) 是一个事实标准头,用于将原始协议(例如 https / https)传递给下游服务器。例如,如果将对 example.com/resource 的请求发送到代理,代理将请求转发到 localhost:8080/resource,则可以发送 X-Forwarded-Ssl: on 来通知服务器原始协议是 https

X-Forwarded-Prefix

虽然不是标准,但 X-Forwarded-Prefix: <prefix> 是一个事实标准头,用于将原始 URL 路径前缀传递给下游服务器。

X-Forwarded-Prefix 的使用因部署场景而异,需要灵活地允许替换、移除或附加目标服务器的路径前缀。

场景 1:覆盖路径前缀

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

前缀是捕获组 {path} 之前的路径的开头。对于代理,前缀是 /api,而对于服务器,前缀是 /app1。在这种情况下,代理可以发送 X-Forwarded-Prefix: /api,以便原始前缀 /api 覆盖服务器前缀 /app1

场景 2:移除路径前缀

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

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/app2

这种部署场景的一个常见情况是按生产应用服务器支付许可费用,为了降低费用,最好在每台服务器上部署多个应用。另一个原因是在同一台服务器上运行更多应用,以共享服务器运行所需的资源。

在这些场景中,应用需要一个非空的上下文根,因为同一台服务器上运行着多个应用。然而,这不应出现在公共 API 的 URL 路径中,公共 API 中应用可以使用不同的子域,从而带来以下好处:

  • 增强安全性,例如,同源策略

  • 应用程序的独立伸缩(不同域名指向不同 IP 地址)

场景 3:插入路径前缀

在其他情况下,可能需要附加一个前缀。例如,考虑以下代理到服务器的映射:

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

在这种情况下,代理的前缀是 /api/app1,服务器的前缀是 /app1。代理可以发送 X-Forwarded-Prefix: /api/app1,以便原始前缀 /api/app1 覆盖服务器前缀 /app1

ForwardedHeaderFilter

ForwardedHeaderFilter 是一个 Servlet 过滤器,它修改请求,以便 a) 根据 Forwarded 头更改主机、端口和方案,b) 移除这些头以消除进一步的影响。该过滤器依赖于包装请求,因此必须排在其他过滤器(如 RequestContextFilter)之前,这些过滤器应处理修改后的请求而不是原始请求。

安全注意事项

对于转发头,存在安全方面的考虑,因为应用程序无法知道这些头是按预期由代理添加的,还是由恶意客户端添加的。因此,应配置信任边界上的代理来移除来自外部的不可信 Forwarded 头。您也可以将 ForwardedHeaderFilter 配置为 removeOnly=true,在这种情况下,它会移除但不使用这些头。

分发器类型

为了支持异步请求和错误分发,此过滤器应映射到 DispatcherType.ASYNCDispatcherType.ERROR。如果使用 Spring Framework 的 AbstractAnnotationConfigDispatcherServletInitializer(参见Servlet 配置),所有过滤器会自动注册到所有分发类型。但是,如果通过 web.xml 或在 Spring Boot 中通过 FilterRegistrationBean 注册过滤器,请务必在 DispatcherType.REQUEST 之外包含 DispatcherType.ASYNCDispatcherType.ERROR

浅层 ETag

ShallowEtagHeaderFilter 过滤器通过缓存写入响应的内容并计算其 MD5 哈希来创建“浅层” ETag。下次客户端发送请求时,它也会这样做,但同时会将计算出的值与 If-None-Match 请求头进行比较,如果两者相等,则返回 304 (NOT_MODIFIED)。

这种策略节省了网络带宽,但不节省 CPU,因为每个请求都必须计算完整的响应。修改状态的 HTTP 方法以及其他 HTTP 条件请求头(如 If-MatchIf-Unmodified-Since)超出了此过滤器的范围。控制器级别的其他策略可以避免计算,并对 HTTP 条件请求提供更广泛的支持。参见HTTP 缓存

此过滤器有一个 writeWeakETag 参数,该参数配置过滤器写入类似于以下内容的弱 ETag:W/"02a2d595e6ed9a0b24f027f2b63b134d6"(如RFC 7232 第 2.3 节中所定义)。

为了支持异步请求,此过滤器必须映射到 DispatcherType.ASYNC,以便过滤器可以延迟并在最后一个异步分发结束时成功生成 ETag。如果使用 Spring Framework 的 AbstractAnnotationConfigDispatcherServletInitializer(参见Servlet 配置),所有过滤器会自动注册到所有分发类型。但是,如果通过 web.xml 或在 Spring Boot 中通过 FilterRegistrationBean 注册过滤器,请务必包含 DispatcherType.ASYNC

CORS

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

有关更多详细信息,请参见CORSCORS 过滤器部分。

URL Handler

在之前的 Spring Framework 版本中,Spring MVC 可以配置为在将传入请求映射到控制器方法时忽略 URL 路径中的尾随斜杠。这可以通过在 PathMatchConfigurer 上启用 setUseTrailingSlashMatch 选项来实现。这意味着发送 "GET /home/" 请求将由使用 @GetMapping("/home") 注解的控制器方法处理。

此选项已弃用,但仍要求应用程序以安全的方式处理此类请求。UrlHandlerFilter Servlet 过滤器就是为此目的而设计的。它可以配置为:

  • 在接收带有尾随斜杠的 URL 时,响应 HTTP 重定向状态,将浏览器重定向到不带尾随斜杠的 URL 变体。

  • 包装请求,使其表现得如同请求不带尾随斜杠一样,并继续处理请求。

以下是如何为一个博客应用实例化和配置 UrlHandlerFilter

  • Java

  • Kotlin

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()