跳到主要内容

过滤器

ChatGPT-4o 中英对照 Filters

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](https://example.com/resource) 被发送到一个代理,并且该代理将请求转发到 [localhost:8080/resource](http://localhost:8080/resource),那么可以发送一个 X-Forwarded-Host: example.com 头来告知服务器原始主机是 example.com

X-Forwarded-Port

虽然 X-Forwarded-Port: <port> 不是标准的,但它是一个事实上的标准头,用于将原始端口传递给下游服务器。例如,如果一个请求 [example.com/resource](https://example.com/resource) 被发送到一个代理,然后代理将请求转发到 [localhost:8080/resource](http://localhost:8080/resource),那么可以发送一个 X-Forwarded-Port: 443 头来告知服务器原始端口是 443

X-Forwarded-Proto

虽然不是标准,X-Forwarded-Proto: (https|http) 是一个事实上的标准头,用于向下游服务器传达原始协议(例如,https / http)。例如,如果一个请求 [example.com/resource](https://example.com/resource) 被发送到一个代理,该代理将请求转发到 [localhost:8080/resource](http://localhost:8080/resource),那么可以发送一个 X-Forwarded-Proto: https 头来告知服务器原始协议是 https

X-Forwarded-Ssl

虽然 X-Forwarded-Ssl: (on|off) 不是标准,但它是一个事实上的标准头,用于将原始协议(例如,https / http)传递给下游服务器。例如,如果一个请求 [example.com/resource](https://example.com/resource) 被发送到一个代理,该代理将请求转发到 [localhost:8080/resource](http://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 路径中可见,因为应用可能使用不同的子域,这带来了以下好处:

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

  • 应用的独立扩展(不同的域指向不同的 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 处理器

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

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

  • 接收到带有斜杠结尾的 URL 时,响应一个 HTTP 重定向状态,将浏览器发送到无斜杠结尾的 URL 变体。

  • 包装请求,使其表现得像是没有斜杠结尾的请求,并继续处理该请求。

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

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