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)