最近在写一个基于Spring Boot和React的网站,使用Rest API进行数据交换。最近遇到了跨源资源共享(CORS)的麻烦,简单讲下原理和解决方案。
CORS
CORS的背景知识参考MDN的文档。为了避免XSS攻击,提升Web的安全性,浏览器默认只允许发送同源的XMLHttpRequest请求。
同源的要求非常高,方法、主机名和端口必须完全相同才能视作同源,参考。也就说http://localhost:3000
和http://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 | Access-Control-Allow-Origin: * |
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 | @Configuration |
controller
在controller方法上加上CrossOrigin注解即可。
1 | @CrossOrigin(origins = "http://localhost:8080") |
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这个在大四上的《信息系统管理》中讲过,当时听的时候感觉没多难,就几个标头发来发去的。但在开发中,没搞懂概念真的写不对代码。