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
)。
全局配置和本地配置的组合规则通常是叠加的——例如,所有全局源和所有本地源。对于那些只接受单个值的属性,例如 allowCredentials
和 maxAge
,本地值会覆盖全局值。有关更多详细信息,请参阅CorsConfiguration#combine(CorsConfiguration)
。
要从源代码了解更多信息或进行高级定制,请参阅:
|
带凭据的请求
对带凭据的请求使用 CORS 需要启用 allowedCredentials
。请注意,此选项与配置的域建立了高度信任,同时通过暴露敏感的用户特定信息(例如 cookie 和 CSRF 令牌)增加了 Web 应用程序的攻击面。
启用凭据还会影响配置的 "*"
CORS 通配符的处理方式
-
allowOrigins
中不允许使用通配符,但可以使用allowOriginPatterns
属性来匹配一组动态来源。 -
在
allowedHeaders
或allowedMethods
上设置时,Access-Control-Allow-Headers
和Access-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 配置来完成此操作。
默认情况下,全局配置启用以下设置:
-
所有来源。
-
所有头。
-
GET
、HEAD
和POST
方法。
默认情况下不启用 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)
}