CORS

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

簡介

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

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

憑據請求

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

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

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

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

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

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

處理

CORS 規範區分了預檢請求(preflight)、簡單請求(simple)和實際請求(actual)。要了解 CORS 的工作原理,您可以閱讀 本文(還有許多其他文章),或檢視規範瞭解更多詳細資訊。

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

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

每個 HandlerMapping 都可以透過基於 URL 模式的 CorsConfiguration 對映進行單獨配置。在大多數情況下,應用程式使用 MVC Java 配置或 XML namespace 來宣告此類對映,這會導致將單個全域性對映傳遞給所有 HandlerMapping 例項。

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

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

要從原始碼中瞭解更多或進行高階定製,請檢視以下程式碼

  • CorsConfiguration

  • CorsProcessor, DefaultCorsProcessor

  • AbstractHandlerMapping

@CrossOrigin

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

  • Java

  • Kotlin

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

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

	@DeleteMapping("/{id}")
	public void remove(@PathVariable Long id) {
		// ...
	}
}
@RestController
@RequestMapping("/account")
class AccountController {

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

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

預設情況下,@CrossOrigin 允許:

  • 所有來源。

  • 所有標頭。

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

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

maxAge 設定為 30 分鐘。

@CrossOrigin 也支援類級別,並會被所有方法繼承,如下例所示:

  • Java

  • Kotlin

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

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

	@DeleteMapping("/{id}")
	public void remove(@PathVariable Long id) {
		// ...
	}
}
@CrossOrigin(origins = ["https://domain2.com"], maxAge = 3600)
@RestController
@RequestMapping("/account")
class AccountController {

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

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

您可以在類級別和方法級別同時使用 @CrossOrigin,如下例所示:

  • Java

  • Kotlin

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

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

	@DeleteMapping("/{id}")
	public void remove(@PathVariable Long id) {
		// ...
	}
}
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
class AccountController {

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

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

全域性配置

除了細粒度的控制器方法級別配置外,您可能還想定義一些全域性 CORS 配置。您可以在任何 HandlerMapping 上單獨設定基於 URL 模式的 CorsConfiguration 對映。然而,大多數應用程式使用 MVC Java 配置或 MVC XML namespace 來實現此目的。

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

  • 所有來源。

  • 所有標頭。

  • GETHEADPOST 方法。

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

maxAge 設定為 30 分鐘。

Java 配置

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

  • Java

  • Kotlin

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

	@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
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

	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...
	}
}

XML 配置

要在 XML namespace 中啟用 CORS,您可以使用 <mvc:cors> 元素,如下例所示:

<mvc:cors>

	<mvc:mapping path="/api/**"
		allowed-origins="https://domain1.com, https://domain2.com"
		allowed-methods="GET, PUT"
		allowed-headers="header1, header2, header3"
		exposed-headers="header1, header2" allow-credentials="true"
		max-age="123" />

	<mvc:mapping path="/resources/**"
		allowed-origins="https://domain1.com" />

</mvc:cors>

CORS 過濾器

您可以透過內建的 CorsFilter 應用 CORS 支援。

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

要配置過濾器,請將 CorsConfigurationSource 傳遞給其建構函式,如下例所示:

  • Java

  • Kotlin

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);

CorsFilter filter = new CorsFilter(source);
val config = CorsConfiguration()

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

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

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

val filter = CorsFilter(source)