篩選器

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*() 系列方法取得。

轉發標頭

當請求通過負載平衡器等 Proxy 時,主機、連接埠和協定可能會變更,這使得從用戶端角度建立指向正確主機、連接埠和協定的連結成為一項挑戰。

RFC 7239 定義了 Forwarded HTTP 標頭,Proxy 可以使用它來提供有關原始請求的資訊。

非標準標頭

還有其他非標準標頭,包括 X-Forwarded-HostX-Forwarded-PortX-Forwarded-ProtoX-Forwarded-SslX-Forwarded-Prefix

X-Forwarded-Host

雖然不是標準的,但 X-Forwarded-Host: <host> 是一個事實上的標準標頭,用於向下游伺服器傳達原始主機。例如,如果將 example.com/resource 的請求傳送到 Proxy,Proxy 將請求轉發到 localhost:8080/resource,則可以傳送 X-Forwarded-Host: example.com 標頭,以告知伺服器原始主機是 example.com

X-Forwarded-Port

雖然不是標準的,但 X-Forwarded-Port: <port> 是一個事實上的標準標頭,用於向下游伺服器傳達原始連接埠。例如,如果將 example.com/resource 的請求傳送到 Proxy,Proxy 將請求轉發到 localhost:8080/resource,則可以傳送 X-Forwarded-Port: 443 標頭,以告知伺服器原始連接埠是 443

X-Forwarded-Proto

雖然不是標準的,但 X-Forwarded-Proto: (https|http) 是一個事實上的標準標頭,用於向下游伺服器傳達原始協定(例如,https/http)。例如,如果將 example.com/resource 的請求傳送到 Proxy,Proxy 將請求轉發到 localhost:8080/resource,則可以傳送 X-Forwarded-Proto: https 標頭,以告知伺服器原始協定是 https

X-Forwarded-Ssl

雖然不是標準的,但 X-Forwarded-Ssl: (on|off) 是一個事實上的標準標頭,用於向下游伺服器傳達原始協定(例如,https/http)。例如,如果將 example.com/resource 的請求傳送到 Proxy,Proxy 將請求轉發到 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} 之前的路徑開頭。對於 Proxy,前綴是 /api,而對於伺服器,前綴是 /app1。在這種情況下,Proxy 可以傳送 X-Forwarded-Prefix: /api,讓原始前綴 /api 覆寫伺服器前綴 /app1

情境 2:移除路徑前綴

有時,應用程式可能想要移除前綴。例如,考慮以下 Proxy 到伺服器的對應

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}

Proxy 沒有前綴,而應用程式 app1app2 的路徑前綴分別為 /app1/app2。Proxy 可以傳送 X-Forwarded-Prefix: ,讓空前綴覆寫伺服器前綴 /app1/app2

此部署情境的常見案例是每個生產應用程式伺服器都需支付授權費用,並且最好在每個伺服器上部署多個應用程式以減少費用。另一個原因是為了在同一伺服器上執行更多應用程式,以共用伺服器執行所需的資源。

在這些情境中,應用程式需要非空的 Context 根目錄,因為同一伺服器上有多個應用程式。但是,這不應在公用 API 的 URL 路徑中可見,在公用 API 中,應用程式可以使用不同的子網域,這樣做可以提供以下優點,例如

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

  • 應用程式的獨立擴展 (不同的網域指向不同的 IP 位址)

情境 3:插入路徑前綴

在其他情況下,可能需要預先加入前綴。例如,考慮以下 Proxy 到伺服器的對應

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

在這種情況下,Proxy 的前綴為 /api/app1,伺服器的前綴為 /app1。Proxy 可以傳送 X-Forwarded-Prefix: /api/app1,讓原始前綴 /api/app1 覆寫伺服器前綴 /app1

ForwardedHeaderFilter

ForwardedHeaderFilter 是一個 Servlet 篩選器,它修改請求以:a) 根據 Forwarded 標頭變更主機、連接埠和協定,以及 b) 移除這些標頭以消除進一步的影響。篩選器依賴於包裝請求,因此它必須排在其他篩選器(例如 RequestContextFilter)之前,這些篩選器應使用修改後的請求而不是原始請求。

安全性考量

轉發標頭存在安全性考量,因為應用程式無法知道標頭是由 Proxy 按預期新增的,還是由惡意用戶端新增的。這就是為什麼應將信任邊界上的 Proxy 配置為移除來自外部不受信任的 Forwarded 標頭。您也可以使用 removeOnly=true 配置 ForwardedHeaderFilter,在這種情況下,它會移除標頭,但不使用它們。

Dispatcher 類型

為了支援非同步請求和錯誤分派,應將此篩選器對應到 DispatcherType.ASYNCDispatcherType.ERROR。如果使用 Spring Framework 的 AbstractAnnotationConfigDispatcherServletInitializer (請參閱Servlet Config),則會自動為所有分派類型註冊所有篩選器。但是,如果透過 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 Config),則會自動為所有分派類型註冊所有篩選器。但是,如果透過 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

  • 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()