过滤器
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-Host
、X-Forwarded-Port
、X-Forwarded-Proto
、X-Forwarded-Ssl
和 X-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}
代理没有前缀,而应用程序 app1
和 app2
分别具有路径前缀 /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.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 (NOT_MODIFIED)。
这种策略节省了网络带宽,但不节省 CPU,因为必须为每个请求计算完整的响应。状态更改的 HTTP 方法和其他 HTTP 条件请求头(如 If-Match
和 If-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 过滤器链的前面。
URL 处理器
在之前的 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()