过滤器

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

表单数据

浏览器只能通过 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} -> https://127.0.0.1:8080/app1/{path}

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

场景 2:移除路径前缀

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

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

代理没有前缀,而应用程序 app1app2 分别具有路径前缀 /app1/app2。代理可以发送 X-Forwarded-Prefix: 来让空前缀覆盖服务器前缀 /app1/app2

这种部署场景的常见情况是,许可证按生产应用程序服务器付费,并且最好在每个服务器上部署多个应用程序以减少费用。另一个原因是在同一台服务器上运行更多应用程序,以便共享服务器运行所需的资源。

在这些场景中,应用程序需要一个非空的上下文根,因为同一台服务器上有多个应用程序。但是,这在公共 API 的 URL 路径中不应该可见,应用程序可以使用不同的子域,这提供了以下好处:

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

  • 应用程序的独立扩展(不同的域指向不同的 IP 地址)

场景 3:插入路径前缀

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

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

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

ForwardedHeaderFilter

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

安全注意事项

转发标头存在安全问题,因为应用程序无法知道标头是按预期由代理添加的,还是由恶意客户端添加的。这就是为什么信任边界处的代理应该配置为移除来自外部的不可信 Forwarded 标头。您还可以使用 removeOnly=true 配置 ForwardedHeaderFilter,在这种情况下,它会移除标头,但不会使用它们。

调度程序类型

为了支持 异步请求 和错误调度,此过滤器应该使用 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 过滤器 的部分。