|
对于最新稳定版本,请使用 Spring Framework 7.0.6! |
过滤器
在 Servlet API 中,你可以添加一个 jakarta.servlet.Filter,以在过滤器处理链和目标 Servlet 的其余处理逻辑之前和之后应用拦截式逻辑。
spring-web 模块包含多个内置的 Filter 实现:
Spring 应用程序中也有可供使用的基类实现:
-
GenericFilterBean— 作为 Spring Bean 配置的Filter的基类; 与 SpringApplicationContext生命周期集成。 -
OncePerRequestFilter—GenericFilterBean的扩展类,支持在请求开始时(即REQUEST分发阶段)仅调用一次,并忽略通过FORWARD分发的后续处理。该过滤器还提供控制选项,决定是否在Filter和ASYNC分发中启用该ERROR。
Servlet 过滤器可以在 web.xml 中配置,也可以通过 Servlet 注解进行配置。
在 Spring Boot 应用程序中,您可以
将 Filter 声明为 Bean,
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*() 系列方法获取表单数据。
转发的请求头
当请求经过负载均衡器等代理时,主机、端口和协议方案可能会发生变化,这使得从客户端视角创建指向正确主机、端口和协议方案的链接变得具有挑战性。
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 的请求发送到代理,而该代理将请求转发到 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 / http)。例如,如果将 example.com/resource 的请求发送到
代理,该代理将请求转发到 localhost:8080/resource,则可以发送一个
X-Forwarded-Proto: https 的头部,以通知服务器原始协议是 https。
X-Forwarded-Ssl
虽然不是标准规范,但 X-Forwarded-Ssl: (on|off) 是一个事实上的标准请求头,用于向下游服务器传递原始协议(例如 http / 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}
代理没有前缀,而应用程序 app1 和 app2 分别具有路径前缀
/app1 和 /app2。代理可以发送 X-Forwarded-Prefix: ,
以使用空前缀覆盖服务器上的前缀 /app1 和 /app2。
|
这种部署场景的一个常见情况是,许可证按生产应用服务器收费,因此更倾向于在每台服务器上部署多个应用程序以降低费用。另一个原因是在同一台服务器上运行更多应用程序,以便共享服务器运行所需的资源。 在这些场景中,应用程序需要一个非空的上下文根路径,因为同一服务器上部署了多个应用程序。然而,在公共 API 的 URL 路径中不应显示该上下文根路径,因为应用程序可能会使用不同的子域名,这样可以带来如下好处:
|
场景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 头信息更改主机、端口和协议(scheme),以及 b) 移除这些头信息,以避免后续产生影响。该过滤器通过包装请求来实现功能,因此必须排在其他过滤器(例如 RequestContextFilter)之前,以确保这些过滤器能够处理修改后的请求,而不是原始请求。
安全注意事项
由于应用程序无法判断转发头(forwarded headers)是由代理按预期添加的,还是由恶意客户端伪造的,因此在使用转发头时存在安全方面的考量。这就是为什么应在信任边界的代理上进行配置,以移除来自外部的不可信 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 处理器
你可能希望你的控制器端点能够匹配 URL 路径中带有或不带尾部斜杠的路由。
例如,"GET /home" 和 "GET /home/" 都应由使用 @GetMapping("/home") 注解的控制器方法处理。
Spring 提供了 UrlHandlerFilter,用于从 URL 路径中移除末尾的斜杠,以确保对带或不带末尾斜杠的路径具有一致的视图。
这对于避免基于 URL 的授权决策与 Web 框架的请求映射之间出现不匹配非常重要。
该过滤器可以通过以下几种方式之一来移除末尾的斜杠:
-
返回一个 HTTP 重定向状态,将客户端重定向到相同路径但不带结尾斜杠的地址。
-
包装请求以移除末尾的斜杠。
历史上,Spring MVC 支持 URL 路径的尾部斜杠匹配。
出于安全原因,该功能在 6.0 版本中已被弃用,并在 7.0 版本中移除,
由 UrlHandlerFilter 提供了一种更安全的替代方案。 |
以下是如何为博客应用程序实例化和配置一个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()
请牢记以下内容:
-
根路径
"/"被排除在尾部斜杠处理之外。 -
@RequestMapping("/")会在类级别的映射路径末尾添加一个斜杠,因此在启用了末尾斜杠处理时将无法匹配;请改用@RequestMapping(不指定 path 属性)。