过滤器
在Servlet API中,你可以添加一个jakarta.servlet.Filter,以便在过滤器处理链的其余部分以及目标Servlet的处理之前和之后应用拦截式的逻辑。
spring-web 模块包含了许多内置的 Filter 实现:
还有用于Spring应用程序的基类实现:
-
GenericFilterBean——作为Spring bean配置的Filter的基类;与SpringApplicationContext的生命周期集成。 -
OncePerRequestFilter——GenericFilterBean的扩展类,支持在请求开始时(即REQUEST分发阶段)仅调用一次该过滤器,并忽略通过FORWARD分发的后续处理。该过滤器还提供了对过滤器是否参与ASYNC和ERROR分发控制的选项。
Servlet过滤器可以在web.xml中配置,也可以通过Servlet注解进行配置。在Spring Boot应用程序中,你可以将过滤器声明为bean,Spring Boot会自动完成它们的配置。
表单数据
浏览器只能通过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*() 系列方法获取表单数据。
转发的头部信息
当请求通过负载均衡器等代理服务器时,主机、端口和协议(scheme)可能会发生变化,这从客户端的角度来看,就使得创建指向正确主机、端口和协议的链接变得具有挑战性。
RFC 7239 定义了 Forwarded HTTP 头部,代理服务器可以使用该头部来提供关于原始请求的信息。
非标准头部
还有其他非标准的头部信息,包括 X-Forwarded-Host、X-Forwarded-Port、X-Forwarded-Proto、X-Forwarded-Ssl、X-Forwarded-Prefix 和 X-Forwarded-For。
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}
代理没有前缀,而应用程序app1和app2分别有路径前缀/app1和/app2。代理可以发送X-Forwarded-Prefix:来覆盖服务器的前缀/app1和/app2,使其变为空前缀。
在这种部署场景中,一个常见的情况是按照每台生产应用服务器来付费,为了减少费用,通常希望在一台服务器上部署多个应用程序。另一个原因是为了在同一台服务器上运行更多应用程序,从而共享该服务器所需的资源。
在这些场景下,由于同一台服务器上运行了多个应用程序,因此需要一个非空的上下文根(context root)。不过,这一设置不应在公共API的URL路径中显现出来,因为应用程序可以使用不同的子域名,这样做有以下好处:
- 增强安全性,例如,可以实现同源策略(same origin policy)。
- 实现应用程序的独立扩展(不同的域名对应不同的IP地址)。
场景3:插入路径前缀
在其他情况下,可能需要添加前缀。例如,考虑以下代理到服务器的映射:
https://example.com/api/app1/{path} -> http://localhost:8080/app1/{path}
在这种情况下,代理的前缀是 /api/app1,服务器的前缀也是 /app1。代理可以发送 X-Forwarded-Prefix: /api/app1,以便让原始前缀 /api/app1 覆盖服务器的前缀 /app1。
X-Forwarded-For
X-Forwarded-For: <address> 是一个事实上的标准头部,用于将客户端的原始 InetSocketAddress 传递给下游服务器。例如,如果一个请求由客户端 [fd00:fefe:1::4] 发送给代理服务器 192.168.0.1,那么 HTTP 请求中包含的“远程地址”信息将反映的是客户端的实际地址,而不是代理服务器的地址。
ForwardedHeaderFilter
ForwardedHeaderFilter 是一种 Servlet 过滤器,它通过修改请求来实现以下功能:a) 根据 Forwarded 头信息更改主机(host)、端口(port)和协议(scheme);b) 移除这些头信息,以避免进一步的影响。该过滤器依赖于对请求的封装处理,因此必须将其置于其他过滤器之前(如 RequestContextFilter),因为这些其他过滤器需要处理的是经过修改后的请求,而非原始请求。
安全考虑
对于转发的头部信息,存在安全方面的考虑:应用程序无法确定这些头部信息是按照预期由代理添加的,还是由恶意客户端添加的。因此,在信任边界处的代理应该被配置为移除来自外部的不可信的“Forwarded”头部信息。你还可以将“ForwardedHeaderFilter”的配置设置为“removeOnly=true”,在这种情况下,该过滤器会移除这些头部信息,但不会将其使用。
分发器类型
为了支持异步请求和错误处理,该过滤器应被映射为DispatcherType.ASYNC以及DispatcherType.ERROR。如果使用Spring Framework的AbstractAnnotationConfigDispatcherServletInitializer(参见servlet配置),所有过滤器将自动为所有调度类型进行注册。然而,如果是通过web.xml注册过滤器,或者在Spring Boot中通过FilterRegistrationBean进行注册,除了DispatcherType.REQUEST之外,务必还包括DispatcherType.ASYNC和DispatcherType>Error。
浅层ETag
ShallowEtagHeaderFilter 这个过滤器通过缓存写入响应的内容并计算其 MD5 哈希值来生成一个“浅层”ETag。下次客户端发送请求时,也会执行相同的操作,但同时还会将计算出的哈希值与请求头中的 If-None-Match 字段进行比较;如果两者相等,就会返回 304(未修改)状态码。
这种策略可以节省网络带宽,但无法节省CPU资源,因为每个请求都必须计算完整的响应内容。会改变状态的HTTP方法以及其他HTTP条件请求头(如If-Match和If-Unmodified-Since)不在该过滤器的适用范围内。在控制器层面采用其他策略可以避免这种计算,同时能更好地支持HTTP条件请求。详情请参见HTTP缓存。
此过滤器具有一个writeWeakETag参数,用于配置过滤器生成类似以下的弱ETag:W/"02a2d595e6ed9a0b24f027f2b63b134d6"(如RFC 7232第2.3节中所定义)。
CORS
Spring MVC通过控制器上的注解提供了细粒度的CORS配置支持。然而,当与Spring Security一起使用时,我们建议依赖于内置的CorsFilter,该过滤器必须排在Spring Security的过滤器链之前。
URL处理器
在之前的Spring Framework版本中,可以配置Spring MVC在映射控制器方法的请求时忽略URL路径末尾的斜杠。这意味着发送“GET /home/”请求将会由一个带有@GetMapping("/home")注解的控制器方法来处理。
此选项在6.0版本中被弃用,在7.0版本中被移除,但应用程序仍需以安全的方式处理此类请求。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()