0%

Spring Boot中配置CORS

最近在写一个基于Spring Boot和React的网站,使用Rest API进行数据交换。最近遇到了跨源资源共享(CORS)的麻烦,简单讲下原理和解决方案。

CORS

CORS的背景知识参考MDN的文档。为了避免XSS攻击,提升Web的安全性,浏览器默认只允许发送同源的XMLHttpRequest请求。

同源的要求非常高,方法、主机名和端口必须完全相同才能视作同源,参考。也就说http://localhost:3000http://localhost:8080是两个不同的源,因为端口不同,浏览器仍不支持发送AJAX请求。

CORS有简单请求和预检请求两种。Post请求发送表单是简单请求,但发送json需要发预检请求。

拿简单请求举例。如果浏览器检测到了请求不同源,会在标头上加上Origin。比如在localhost中测试React,则会发送http://localhost:3000

1
Origin: http://localhost:3000

如果服务器支持这个源站向它发送跨域请求,则返回标头Access-Control-Allow-Origin:http://localhost:3000。支持所有网站跨域则返回通配符。

1
2
3
4
5
Access-Control-Allow-Origin: *

or

Access-Control-Allow-Origin: http://localhost:3000

Spring配置

Spring有多种配置CORS的方法,参见官方教程

全局

添加一个WebMvcConfigurer的Bean,方法挪到启动main也是OK的。默认支持PUT, GET和Head方法,其他的可以用allowedMethods方法进行添加。maxAge默认为30分钟。

如果需要添加多个origin,直接在allowedOrigins方法里面加就行,比如allowedOrigins( "http://localhost:3000", "http://192.168.1.1:3000")

下面是Kotlin代码,Java代码在上面的官方教程里面有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
open class CorsFilter {

@Bean
open fun addCorsFilter(): WebMvcConfigurer
{
return object : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/**")
.allowedOrigins( "http://localhost:3000")
}
}
}
}

controller

在controller方法上加上CrossOrigin注解即可。

1
2
3
@CrossOrigin(origins = "http://localhost:8080")
@RequestMapping("/location")
fun controllerMethod(){}

credentials

很多示例中把allowCredentials设置为了true,这个设置是有限制的

为了避免CSRF攻击,默认情况下浏览器和服务器都不允许跨源发送cookie,哪怕服务器允许发送跨源请求。如果没有跨源发送cookies等数据的需求的话,不应该设置该项,直接默认为false即可,参见MDN教程

如果要发送cookie信息,需要前端和后端同时允许,参考What exactly does the Access-Control-Allow-Credentials header do?。React需要在Axios中加入withCredentials属性,并设置为true。

Spring需要在registry中调用allowCredentials(true)方法。特别注意,如果设置了allowCredentials为true,则不允许设置allowedOrigins为通配符*,参考Reason: Credential is not supported if the CORS header ‘Access-Control-Allow-Origin’ is ‘*’

如果服务器设置允许,则会返回浏览器一个Access-Control-Allow-Credentials: true的标头。如果缺少这个标头,js无法从浏览器拿到返回的数据,参考Access-Control-Allow-Credentials

这个机制让恶意脚本无法往一台withCredentials为false的服务器发送用户的cookie来攻击,浏览器会拦截收到的数据,也无法往一台withCredentials为true的服务器发送请求,因为allowedOrigins不允许设置为通配符。

预检请求

CORS的预检请求由浏览器去实现,参考MDN文档

首先浏览器发送一个Option请求,带有Origin, Access-Control-Request-Method和Access-Control-Request-Headers三个标头,看看服务器接受哪些源、方法和标头。服务器会回复Access-Control-Allow-Origin, Access-Control-Allow_Method, Access-Control-Allow-Headers和Access-Control-Max-Age四个标头。第二个请求和上面讲的简单请求一样。

拿登录请求测试下。因为发送的是json格式的post请求,Content-Type为application/json,不满足简单请求的条件。

预检请求

检查这个预检请求的标头。请求方法为post,回复允许get, head和post。如果要用delete等方法需要在CorsRegistry中设置。标头请求content-type,回复允许content-type。回复的最长时间为1800秒(30分钟),也就是30分钟内都不用再发预检请求,否则次次都两次请求,延时太高了。

标头

这个预检请求会留下一个炸弹。如果用filter或者interceptor来进行鉴权的话,要对Option请求方法进行单独处理,因为它不会带正式请求的数据,正式请求在下一个。

结尾

CORS这个在大四上的《信息系统管理》中讲过,当时听的时候感觉没多难,就几个标头发来发去的。但在开发中,没搞懂概念真的写不对代码。