CORS
Spring WebFlux允许你处理CORS(跨源资源共享)。本节将描述如何实现这一点。
介绍
出于安全考虑,浏览器禁止对当前源域之外的资源进行AJAX调用。例如,你可能在一个标签页中查看银行账户信息,在另一个标签页中访问evil.com网站。来自evil.com的脚本不应能够使用你的账户凭据向你的银行API发起AJAX请求——比如从你的账户中取款!
处理
CORS规范区分了预检请求(preflight requests)、简单请求(simple requests)和实际请求(actual requests)。要了解CORS的工作原理,你可以阅读这篇文章以及其他相关资料,或者查阅规范以获取更多细节。
Spring WebFlux的HandlerMapping实现提供了对CORS(跨源资源共享)的内置支持。在成功将请求映射到相应的处理程序之后,HandlerMapping会检查该请求和处理程序的CORS配置,并据此采取进一步操作。预检请求(preflight requests)会被直接处理,而普通的CORS请求则会被拦截、验证,并设置所需的CORS响应头。
为了启用跨源请求(即存在 Origin 标头,并且该标头与请求的主机不同),你需要有一些明确声明的 CORS 配置。如果没有找到匹配的 CORS 配置,预检请求将被拒绝。简单请求和实际的 CORS 请求的响应中不会添加任何 CORS 标头,因此浏览器会拒绝这些请求。
每个HandlerMapping都可以通过基于URL模式的CorsConfiguration映射来单独配置([配置链接:https://docs.spring.io/spring-framework/docs/7.0.3/javadoc-api/org/springframework/web/reactive/handler/AbstractHandlerMapping.html#setCorsConfigurations(java.util.Map))。在大多数情况下,应用程序使用WebFlux Java配置来声明此类映射,这样就会得到一个全局的映射,该映射会被传递给所有的HandlerMapping实现。
你可以在HandlerMapping级别进行全局CORS配置,同时也可以进行更细粒度的、在处理程序(handler)级别的CORS配置。例如,带有注解的控制器可以使用类级或方法级的@CrossOrigin注解(其他处理程序可以实现CorsConfigurationSource)。
全局配置和局部配置结合的规则通常是相加的——例如,所有全局配置的属性值与所有局部配置的属性值都会被一起考虑。对于那些只能接受单个值的属性(如 allowCredentials 和 maxAge),局部配置的值会覆盖全局配置的值。更多详细信息请参见 CorsConfiguration#combine(CorsConfiguration)。
如需从源代码中学习更多内容或进行高级定制,请参阅:
CorsConfigurationCorsProcessor和DefaultCorsProcessorAbstractHandlerMapping
带凭据的请求
在使用带凭据的请求(credentialed requests)时,需要启用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 默认是未启用的,因为启用该功能会建立一种信任关系,从而暴露用户的敏感信息(如 cookies 和 CSRF tokens),因此应仅在适当的情况下使用。当启用该功能时,要么需要将 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 MonoVoid> remove(@PathVariable Long id) {
// ...
}
}
在类级别使用
@CrossOrigin。在方法级别使用
@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) {
// ...
}
}
在类级别使用
@CrossOrigin。在方法级别使用
@CrossOrigin。
全局配置
除了细粒度的控制器方法级配置外,你可能还想定义一些全局的CORS配置。你可以单独在任何HandlerMapping上设置基于URL的CorsConfiguration映射。然而,大多数应用程序使用WebFlux Java配置来实现这一目标。
默认情况下,全局配置启用以下功能:
- 所有来源。
- 所有请求头。
GET、HEAD和POST方法。
allowedCredentials 默认情况下是未启用的,因为启用该功能会暴露敏感的用户特定信息(如 cookie 和 CSRF 令牌),因此应该仅在适当的情况下使用。当启用该功能时,要么需要将 allowOrigins 设置为一个或多个具体的域名(但不能设置为特殊值 „*“),要么可以使用 allowOriginPatterns 属性来匹配一组动态的来源地址。
maxAge 被设置为 30 分钟。
要在WebFlux Java配置中启用CORS,可以使用CorsRegistry回调,如下例所示:
- Java
- Kotlin
@Configuration
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
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)
}