CORS

Spring WebFlux 可讓您處理 CORS(跨來源資源共享)。本節說明如何執行此操作。

簡介

基於安全考量,瀏覽器禁止對目前來源以外的資源進行 AJAX 呼叫。例如,您可能在一個分頁中有您的銀行帳戶,而在另一個分頁中有 evil.com。來自 evil.com 的腳本不應能夠使用您的憑證對您的銀行 API 提出 AJAX 請求,例如從您的帳戶中提款!

跨來源資源共享 (CORS) 是一種 W3C 規範,由 大多數瀏覽器 實作,可讓您指定授權哪些類型的跨網域請求,而不是使用基於 IFRAME 或 JSONP 的較不安全且功能較弱的變通方法。

處理

CORS 規範區分預檢、簡單和實際請求。若要了解 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 令牌)來增加網路應用程式的攻擊面。

啟用憑證也會影響如何處理組態的 "*" 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 方法。

allowCredentials 預設為未啟用,因為這會建立信任層級,進而公開敏感的使用者特定資訊(例如 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)
}