CORS

Spring WebFlux 允許你處理 CORS(跨域資源共享)。本節介紹如何進行處理。

引言

出於安全原因,瀏覽器禁止 AJAX 呼叫當前源之外的資源。例如,你可以在一個標籤頁中開啟你的銀行賬戶,在另一個標籤頁中開啟 evil.com。來自 evil.com 的指令碼不應能夠使用你的憑據對你的銀行 API 傳送 AJAX 請求——例如,從你的賬戶中取款!

跨域資源共享 (CORS) 是由大多數瀏覽器實現的 W3C 規範,它允許你指定授權哪種跨域請求,而不是使用基於 IFRAME 或 JSONP 的不安全且功能較弱的變通方法。

處理

CORS 規範區分預檢請求(preflight)、簡單請求(simple)和實際請求(actual)。要了解 CORS 的工作原理,你可以閱讀這篇文章,或檢視規範瞭解更多詳情。

Spring WebFlux 的 HandlerMapping 實現內建了對 CORS 的支援。成功將請求對映到處理程式後,HandlerMapping 會檢查給定請求和處理程式的 CORS 配置並採取進一步操作。預檢請求直接處理,而簡單請求和實際 CORS 請求會被攔截、驗證並設定所需的 CORS 響應頭。

為了啟用跨域請求(即存在 Origin 頭且與請求的主機不同),你需要有一些顯式宣告的 CORS 配置。如果找不到匹配的 CORS 配置,預檢請求將被拒絕。簡單請求和實際 CORS 請求的響應中不會新增 CORS 頭,因此瀏覽器會拒絕它們。

每個 HandlerMapping 都可以透過基於 URL 模式的 CorsConfiguration 對映進行單獨配置。在大多數情況下,應用程式使用 WebFlux Java 配置來宣告這些對映,這會生成一個傳遞給所有 HandlerMapping 實現的單一全域性對映。

你可以將 HandlerMapping 級別的全域性 CORS 配置與更細粒度的處理程式級別 CORS 配置結合使用。例如,帶註解的控制器可以使用類級別或方法級別的 @CrossOrigin 註解(其他處理程式可以實現 CorsConfigurationSource)。

全域性配置和本地配置的組合規則通常是疊加的——例如,所有全域性源和所有本地源。對於那些只接受單個值的屬性,例如 allowCredentialsmaxAge,本地值會覆蓋全域性值。有關更多詳細資訊,請參閱CorsConfiguration#combine(CorsConfiguration)

要從原始碼瞭解更多資訊或進行高階定製,請參閱:

  • CorsConfiguration

  • CorsProcessorDefaultCorsProcessor

  • AbstractHandlerMapping

帶憑據的請求

對帶憑據的請求使用 CORS 需要啟用 allowedCredentials。請注意,此選項與配置的域建立了高度信任,同時透過暴露敏感的使用者特定資訊(例如 cookie 和 CSRF 令牌)增加了 Web 應用程式的攻擊面。

啟用憑據還會影響配置的 "*" CORS 萬用字元的處理方式

  • allowOrigins 中不允許使用萬用字元,但可以使用 allowOriginPatterns 屬性來匹配一組動態來源。

  • allowedHeadersallowedMethods 上設定時,Access-Control-Allow-HeadersAccess-Control-Allow-Methods 響應頭透過複製 CORS 預檢請求中指定的相關頭和方法來處理。

  • exposedHeaders 上設定時,Access-Control-Expose-Headers 響應頭被設定為配置的頭列表或萬用字元。雖然 CORS 規範不允許在 Access-Control-Allow-Credentials 設定為 true 時使用萬用字元,但大多數瀏覽器都支援它,並且在 CORS 處理期間並非所有響應頭都可用,因此,無論 allowCredentials 屬性的值如何,指定時都使用萬用字元作為頭值。

雖然這種萬用字元配置很方便,但建議在可能的情況下配置有限的一組值,以提供更高的安全性。

@CrossOrigin

@CrossOrigin 註解在帶註解的控制器方法上啟用跨域請求,示例如下:

  • Java

  • Kotlin

@RestController
@RequestMapping("/account")
public class AccountController {

	@CrossOrigin
	@GetMapping("/{id}")
	public Mono<Account> retrieve(@PathVariable Long id) {
		// ...
	}

	@DeleteMapping("/{id}")
	public Mono<Void> remove(@PathVariable Long id) {
		// ...
	}
}
@RestController
@RequestMapping("/account")
class AccountController {

	@CrossOrigin
	@GetMapping("/{id}")
	suspend fun retrieve(@PathVariable id: Long): Account {
		// ...
	}

	@DeleteMapping("/{id}")
	suspend fun remove(@PathVariable id: Long) {
		// ...
	}
}

預設情況下,@CrossOrigin 允許:

  • 所有來源。

  • 所有頭。

  • 控制器方法對映到的所有 HTTP 方法。

預設情況下不啟用 allowCredentials,因為它建立了暴露敏感使用者特定資訊(例如 cookie 和 CSRF 令牌)的信任級別,應僅在適當的情況下使用。啟用此選項時,allowOrigins 必須設定為一個或多個特定域(但不能是特殊值 "*"),或者可以使用 allowOriginPatterns 屬性來匹配一組動態來源。

maxAge 設定為 30 分鐘。

@CrossOrigin 也支援類級別,並被所有方法繼承。以下示例指定了某個域並將 maxAge 設定為一小時:

  • Java

  • Kotlin

@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

	@GetMapping("/{id}")
	public Mono<Account> retrieve(@PathVariable Long id) {
		// ...
	}

	@DeleteMapping("/{id}")
	public Mono<Void> remove(@PathVariable Long id) {
		// ...
	}
}
@CrossOrigin("https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
class AccountController {

	@GetMapping("/{id}")
	suspend fun retrieve(@PathVariable id: Long): Account {
		// ...
	}

	@DeleteMapping("/{id}")
	suspend fun remove(@PathVariable id: Long) {
		// ...
	}
}

你可以在類級別和方法級別使用 @CrossOrigin,示例如下:

  • Java

  • Kotlin

@CrossOrigin(maxAge = 3600) (1)
@RestController
@RequestMapping("/account")
public class AccountController {

	@CrossOrigin("https://domain2.com") (2)
	@GetMapping("/{id}")
	public Mono<Account> retrieve(@PathVariable Long id) {
		// ...
	}

	@DeleteMapping("/{id}")
	public Mono<Void> remove(@PathVariable Long id) {
		// ...
	}
}
1 在類級別使用 @CrossOrigin
2 在方法級別使用 @CrossOrigin
@CrossOrigin(maxAge = 3600) (1)
@RestController
@RequestMapping("/account")
class AccountController {

	@CrossOrigin("https://domain2.com") (2)
	@GetMapping("/{id}")
	suspend fun retrieve(@PathVariable id: Long): Account {
		// ...
	}

	@DeleteMapping("/{id}")
	suspend fun remove(@PathVariable id: Long) {
		// ...
	}
}
1 在類級別使用 @CrossOrigin
2 在方法級別使用 @CrossOrigin

全域性配置

除了細粒度的控制器方法級別配置外,你可能還希望定義一些全域性 CORS 配置。你可以在任何 HandlerMapping 上單獨設定基於 URL 的 CorsConfiguration 對映。然而,大多數應用程式使用 WebFlux Java 配置來完成此操作。

預設情況下,全域性配置啟用以下設定:

  • 所有來源。

  • 所有頭。

  • GETHEADPOST 方法。

預設情況下不啟用 allowedCredentials,因為它建立了暴露敏感使用者特定資訊(例如 cookie 和 CSRF 令牌)的信任級別,應僅在適當的情況下使用。啟用此選項時,allowOrigins 必須設定為一個或多個特定域(但不能是特殊值 "*"),或者可以使用 allowOriginPatterns 屬性來匹配一組動態來源。

maxAge 設定為 30 分鐘。

要在 WebFlux Java 配置中啟用 CORS,你可以使用 CorsRegistry 回撥,示例如下:

  • Java

  • Kotlin

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

	@Override
	public void addCorsMappings(CorsRegistry registry) {

		registry.addMapping("/api/**")
			.allowedOrigins("https://domain2.com")
			.allowedMethods("PUT", "DELETE")
			.allowedHeaders("header1", "header2", "header3")
			.exposedHeaders("header1", "header2")
			.allowCredentials(true).maxAge(3600);

		// Add more mappings...
	}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

	override fun addCorsMappings(registry: CorsRegistry) {

		registry.addMapping("/api/**")
				.allowedOrigins("https://domain2.com")
				.allowedMethods("PUT", "DELETE")
				.allowedHeaders("header1", "header2", "header3")
				.exposedHeaders("header1", "header2")
				.allowCredentials(true).maxAge(3600)

		// Add more mappings...
	}
}

CORS WebFilter

你可以透過內建的CorsWebFilter 應用 CORS 支援,這與函式式端點很契合。

如果你嘗試將 CorsFilter 與 Spring Security 一起使用,請記住 Spring Security 內建支援 CORS。

要配置過濾器,你可以宣告一個 CorsWebFilter bean 並將 CorsConfigurationSource 傳遞給它的建構函式,示例如下:

  • Java

  • Kotlin

@Bean
CorsWebFilter corsFilter() {

	CorsConfiguration config = new CorsConfiguration();

	// Possibly...
	// config.applyPermitDefaultValues()

	config.setAllowCredentials(true);
	config.addAllowedOrigin("https://domain1.com");
	config.addAllowedHeader("*");
	config.addAllowedMethod("*");

	UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
	source.registerCorsConfiguration("/**", config);

	return new CorsWebFilter(source);
}
@Bean
fun corsFilter(): CorsWebFilter {

	val config = CorsConfiguration()

	// Possibly...
	// config.applyPermitDefaultValues()

	config.allowCredentials = true
	config.addAllowedOrigin("https://domain1.com")
	config.addAllowedHeader("*")
	config.addAllowedMethod("*")

	val source = UrlBasedCorsConfigurationSource().apply {
		registerCorsConfiguration("/**", config)
	}
	return CorsWebFilter(source)
}