CORS
Spring MVC允许你处理CORS(跨源资源共享)。本节将描述如何实现这一点。
介绍
出于安全考虑,浏览器禁止对当前域名(origin)之外的资源进行AJAX调用。例如,你可能在一个标签页中打开了自己的银行账户页面,在另一个标签页中打开了evil.com网站。来自evil.com的脚本不应该能够使用你的用户名和密码向你的银行API发起AJAX请求——比如从你的账户中取款!
带凭据的请求
在使用带凭据的请求时结合CORS,需要启用allowedCredentials选项。请注意,此选项会与配置的域名建立高度信任关系,但同时也会增加Web应用程序的攻击面,因为会暴露诸如cookie和CSRF令牌等敏感的用户特定信息。
启用凭据也会影响配置的“*”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属性的值如何,都会使用通配符作为Access-Control-Expose-Headers的值。
虽然这种通配符配置可能很方便,但建议在可能的情况下配置一组有限的值,以提供更高的安全性。
处理
CORS规范区分了预检(preflight)、简单请求(simple request)和实际请求(actual request)。要了解CORS的原理,你可以阅读这篇文章以及其他相关资料,或者查看规范文档以获取更多细节。
Spring MVC的HandlerMapping实现提供了对CORS(跨源资源共享)的内置支持。在成功将请求映射到相应的处理程序之后,HandlerMapping实现会检查该请求和处理程序的CORS配置,并根据需要采取进一步措施。预检请求(preflight requests)会被直接处理,而普通的CORS请求则会被拦截、验证,并设置所需的CORS响应头。
为了支持跨源请求(即Origin头存在且与请求的主机不同),你需要有一些明确声明的CORS配置。如果没有找到匹配的CORS配置,预检请求将会被拒绝。对于简单请求和实际的CORS请求,其响应中不会添加任何CORS头,因此浏览器会拒绝这些请求。
每个 HandlerMapping 都可以单独通过基于 URL 模式的 CorsConfiguration 映射来 配置。在大多数情况下,应用程序使用 MVC Java 配置或 XML 命名空间来声明此类映射,这会导致一个全局映射被传递给所有的 HandlerMapping 实例。
你可以在 HandlerMapping 级别进行全局 CORS 配置,同时也可以进行更细粒度的、处理程序级别的 CORS 配置。例如,带有注解的控制器可以使用类级别或方法级别的 @CrossOrigin 注解(其他处理程序可以实现 CorsConfigurationSource)。
全局配置与本地配置结合的规则通常是相加的——例如,所有全局配置的设置和所有本地配置的设置会一起生效。对于那些只能接受单一值的属性(如 allowCredentials 和 maxAge),本地配置的值会覆盖全局配置的值。更多详细信息,请参阅 CorsConfiguration#combine(CorsConfiguration)。
若想从源代码中了解更多信息或进行高级定制,请查看以下组件背后的代码:
CorsConfigurationCorsProcessor,DefaultCorsProcessorAbstractHandlerMapping
@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命名空间来完成这一任务。
默认情况下,全局配置启用以下功能:
- 所有来源。
- 所有头部信息(headers)。
GET、HEAD和POST方法。
allowCredentials 默认是未启用的,因为这会建立一种信任机制,从而暴露敏感的用户特定信息(如 cookie 和 CSRF 令牌),因此只应在适当的情况下使用。当启用该功能时,必须将 allowOrigins 设置为一个或多个具体的域名(但不能是特殊值 “*”),或者可以使用 allowOriginPatterns 属性来匹配一组动态的来源地址。
maxAge 被设置为 30 分钟。
你可以在Spring MVC配置中启用CORS,如下例所示:
- Java
- Kotlin
- Xml
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain1.com", "https://domain2.com")
.allowedMethods("GET", "PUT")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true)
.maxAge(3600);
// Add more mappings...
}
}
@Configuration
class WebConfiguration : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain1.com", "https://domain2.com")
.allowedMethods("GET", "PUT")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true)
.maxAge(3600)
// Add more mappings...
}
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<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>
</beans>
CORS过滤器
你可以通过内置的 CorsFilter 来应用 CORS 支持。
如果你尝试在Spring Security中使用CorsFilter,请记住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)