本文来自于公众号链接: 彻底掌握CORS跨源资源共享
本文接上篇公众号文章:彻底理解浏览器同源策略SOP
一.概述
在云时代,各种SAAS应用层出不穷,各种互联网API接口越来越丰富,H5技术在微信小程序、支付宝小程序、Hybird中大行其道,所有的这些都离不开跨源访问。
CORS即跨源资源共享(Cross-Origin Resource Sharing),是由W3C组织维护的处于稳定状态的浏览器跨源访问规范,被现代主流版本浏览器充分支持。在普通的web应用跨源访问server的场景下,CORS是最优的跨源访问方案。对比其他的方案,如iframe标签嵌套方案不够安全,JSONP方案功也只支持GET方法,CORS的安全性高且功能完善。
什么是跨源访问
现代浏览器都支持同源策略SOP。假如有网站:
A的web页调用B的资源,此时因为A和B的源不同,就发生了跨源访问。根据SOP规范,在默认情况下A的web页是无法访问到B的资源的。CORS在尽量保证安全的前提下,放宽了SOP限制,使得浏览器可以跨源访问服务器资源。
关于SOP细则,请参考上篇公众号文章:彻底理解浏览器同源策略SOP
插图:corsflow
除了最常用的XMLHttpRequest(AJAX)或Fetch发起的跨源请求,web元素如Form表单、跨源加载css、web字体、甚至3D图形引擎webGL跨源加载纹理等等都可以发起跨源请求。
本文通过Form表单和AJAX跨源访问代码示例来讲解CORS的来龙去脉。虽然代码示例是基于Java技术栈的Spring Boot+Spring Security框架,但是CORS流程原理是多语言栈通用的,因此不影响其他语言栈的同学阅读。
一.Form默认支持跨源访问
Form实际上是默认支持跨源访问的,CORS在发展过程中一直在努力保持不与Form冲突,这个问题常常被开发者忽略。
Form是从HTTP1.0就存在的历史遗留技术,至今仍然被广泛应用。Form没有AJAX功能强大,产生危害的可能性更小,支持的Http方法只有GET和POST,支持的Http头很少而且还无法自定义扩展。其次,Form请求成功后是全页面刷新的,当使用Form提交到其他地址,原页面会重定向到其他地址,使得脚本无法获取新页面中的内容做恶意篡改。浏览器有足够的理由认为即使Form跨源访问也是安全的。
我们以Java程序为例,体验下Form默认支持的跨源访问。
新建一个Spring Boot工程,命名为“back-end-spring-boot",端口号为8080,依赖spring-boot-starter-web包,新增一个controller接口:
@RestController
public class SampleController {
@GetMapping("/sample")
public String getSample() {
return "getSample";
}
@PostMapping("/sample")
public String post(HttpServletRequest request) {
return "post";
}
@DeleteMapping("/sample")
public String delete(String sampleId) {
return "delete";
}
@PutMapping("/sample")
public String put(HttpServletRequest request) {
return "put";
}
}
SampleController提供了基础的GET,POST,PUT,DELETE方法类型的接口。
在工程目录“resources/static/”下新建一个html5文件index.html,添加一个Form表单:
<form action="http://localhost:8080/sample" method="POST" >
<div >
<label for="name">Enter your name: </label>
<input type="text" name="name" id="name" required>
</div>
<div >
<label for="name">Enter your password: </label>
<input type="text" name="password" id="password" required>
</div>
<div>
<input type="submit" value="提交">
</div>
</form>
运行
- 运行工程“back-end-spring-boot"
- 双击index.html,使用浏览器的file协议打开页面。此时index.html和“back-end-spring-boot"是不同的源,可以模拟跨源访问。
- 点击index.html的“提交”按钮。
插图;form
浏览器成功重定向到:http://localhost:8080/sample ,并且页面显示字符串“post”。
插图;formresult。
此示例演示了工程“back-end-spring-boot"中并没有任何跨源策略,Form就可以进行跨源访问。
二.AJAX使用CORS跨源访问
与Form不同,XMLHttpRequest(AJAX)功能更灵活更强大,也更容易带来安全风风险。AJAX可以实现web的局部刷新,并且每次请求可以直接读取响应内容,浏览器认为AJAX的跨源访问是危险的。
1.跨源访问错误
使用AJAX无法直接跨源访问server。
在index.xml增加一个模拟AJAX请求的按钮:
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script language="javascript" type="text/javascript">
function getSample() {
$.ajax({
type: 'GET',
url: 'http://localhost:8080/sample',
success: function (result) {
alert(result);
},
error: function (error, msg) {
alert(msg);
}
});
}
</script>
<button onclick="getSample()">getSample</button>
双击index.html,使用浏览器查看,此时点击getSample按钮,在浏览器控制台会提示一个跨源访问错误:
图片:withoutCORS
在同源策略限制下,浏览器虽然可以访问到server端,server端也会正常返回数据,但是返回的数据会被浏览器拦截,web应用只能收到一个跨源访问错误信息。
2.使用CORS跨源访问
如果AJAX想跨源访问server,需要在server端配置CORS规则。
server端工程“back-end-spring-boot”支持CORS,
首先引入Spring Security包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
再增加security配置类:
@EnableWebSecurity(debug = true)
@Profile("defaultCORS")
public class SecurityConfigDefaultCORS extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().permitAll();
http.cors();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
}
Spring Boot框架的 org.springframework.web.filter.CorsFilter 过滤器实现了CORS功能。如果又使用了Spring Security,只需要调用http.cors(),框架就会自动构造一个CorsFilter的实例。CorsConfigurationSource实例负责CORS具体规则的全局配置。其中“corsConfiguration.addAllowedOrigin("*");”表示允许所有源访问,实际是server端返回时候添加“Access-Control-Allow-Origin”响应头。
重新启动工程“back-end-spring-boot”,再双击index.html,在浏览器查看,点击getSample按钮,弹出框显示:
插图:ajaxcors
此时AJAX可以使用GET方法跨源访问http://localhost:8080/sample接口。
大部分Web Server只需要一个过滤器类就实现了CORS的处理逻辑。以Java技术栈的tomcat为例,它提供了 org.apache.catalina.filters.CorsFilter 来实现CORS功能。对于不便修改源码的项目,直接修改tomcat配置就可以快速支持CORS。
3.CORS的响应头Access-Control-Allow-Methods控制允许的Http方法
上个示例AJAX使用GET方法进行了跨源访问。如果同时要支持POST方法跨源访问,则修改server端的响应头Access-Control-Allow-Methods,来控制允许访问的HTTP方法。如:
Access-Control-Allow-Methods: POST, GET, OPTIONS
表示允许浏览器使用 POST, GET 和 OPTIONS 方法发起请求。
在index.html增加:
<script language="javascript" type="text/javascript">
function postSample() {
var postParam = {
'username':"a",
'password':"a"
};
$.ajax({
type: 'POST',
url: 'http://localhost:8080/sample',
data:postParam,
success: function (result) {
alert(result);
},
error: function (error, msg) {
alert(msg);
}
});
}
</script>
<button onclick="postSample()">postSample</button>
双击index.html,在浏览器打开页面后点击"postSample"按钮,浏览器报错:
插图:cors错误
再security配置类的corsConfigurationSource方法中增加:
corsConfiguration.addAllowedMethod("*");
星号代表允许所有Http方法如GET,POST,OPTIONS,DELETE等等。
并禁用csrf保护(csrf保护不是本篇重点,禁用后,可以减少不必要的干扰)
http.csrf().disable();
刷新index.html,在浏览器打开页面后点击"postSample"按钮,浏览器显示:
插图:postsuccess
此时就可以跨源访问"postSample"接口了。
4.CORS的响应头Access-Control-Allow-Headers控制允许携带的Http头
除了可以控制允许的Http方法,在server端的响应头Access-Control-Allow-Headers可以控制跨源请求中允许携带的Http头。如:
Access-Control-Allow-Headers: X-TEST, Y-TEST
表示跨源访问时允许携带自定义头X-TEST和Y-TEST
在index.html的postSample方法请求时候传递自定义头:
headers: {
'X-TEST':"test header"
},
刷新index.html页面,点击postSample按钮会报错:
插图:allowheader
再在security的配置类增加:
corsConfiguration.addAllowedHeader("*");
星号(“*”)表示的允许所有请求头。
再次访问则提示成功,并且server端可以调用“request.getHeader(“X-TEST”)”获取自定义的Http头信息 。
5.CORS的其他响应头
CORS还定义了其他server端可配置的响应头,通过这些响应头可以细粒度地控制跨源访问规则。具体的有:
- Access-Control-Allow-Origin:允许跨源访问的源列表
- Access-Control-Allow-Methods:允许跨源访问的Http方法
- Access-Control-Allow-Headers:允许跨源访问时候携带的Http头
- Access-Control-Max-Age:预检请求缓存的时间
- Access-Control-Expose-Headers:server端允许客户端访问的响应头列表
- Access-Control-Allow-Credentials:允许浏览器携带如cookie等用户验证信息
Access-Control-Expose-Headers
其中的Access-Control-Expose-Headers在前后端分离架构中经常会用到,比如登录成功后server端经常通过Http响应头回传给浏览器一个token,如:
Authorization: 71f0ed0aa56d480a81ce78eb8cc99605
并且设置:
corsConfiguration.addExposedHeader(HttpHeaders.AUTHORIZATION);
此时浏览器调用getResponseHeader()可以获取到Authorization中的token值,然后缓存这个token,表示用户已经登录成功。
需要注意,CORS安全性规则定义Access-Control-Expose-Headers不可以设置为星号“*”。
Access-Control-Allow-Credentials
其中的Access-Control-Allow-Credentials最常用的场景是浏览器跨源访问时可以携带跨源的cookie,也就是携带用户验证状态。比如在单点退出场景下,server A退出时候同时,浏览器同时跨源调用server B的退出接口,此时配置:
corsConfiguration.setAllowCredentials(true);
并且AJAX调用时候配置:
$.ajax({
...
xhrFields: {
withCredentials: true
},
crossDomain: true,: true,
...
})
此时Server A跨源访问此时Server B时,可以携带着此时Server B的cookie,此时Server B就可以拿到这个cookie进行退出操作了。
三.CORS流程原理
CORS定义了一系列Http头,定义了浏览器和Server之间的交互流程,使得CORS实现浏览器可以安全地跨源访问server,并且server可以细粒度地控制跨源访问规则。
对于Http头来说,除了server端的响应头之外,CORS还规定了浏览器端的请求头:
- Origin:表示发起的跨源请求的源。如A跨源访问B,则Origin的值是A的源(schema,host,port),server端只有接收到携带Origin头的请求时,才会认为这是一个跨源请求。
- Access-Control-Request-Method:用在预检请求过程中,代表实际请求的Http方法类型
- Access-Control-Request-Headers:用在预检请求过程中,代表实际请求需要携带的自定义Http头
CORS协议是由最初的access-control访问提案逐渐演变过来的,因此这些CORS都以“Access-Control-”为前缀。
对于跨源访问流程来说,CORS本来可以简化为大致两个步骤:
- 浏览器发起OPTIONS方法类型的预检请求(Preflight)
- server端验证预检请求通过后,浏览器再发送真实请求
然而CORS为了同时兼容Form和AJAX、为了向下兼容HTTP协议和浏览器,无法做到这么简单。比如FORM表单也必须可以跨源访问却无法支持OPTIONS类型的预检请求。
因此CORS将浏览器请求分为两种:
- 简单跨源请求(Simple Cross-Origin Request)
- 需要预检的跨源请求(Cross-Origin Request with Preflight)
1.简单跨源请求
简单跨源请求主要是为了使Form表单等技术也支持跨源访问,使得CORS协议可以向上和向下兼容。
同时满足“简单HTTP方法”、“简单HTTP头”的请求,可视为“简单请求”。
简单HTTP方法(Simple Method)
- GET
- HEAD
- POST
GET,POST,HEAD方法是HTTP1.0协议就定义的,Form也只支持GET和POST。
简单HTTP头(Simple Header)
- Accept
- Accept-Language
- Content-Language
- Content-Type取值为:
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
注意POST跨源请求时,如果Content-Type不是上述的三种,则不是简单跨源请求。
无论是“简单HTTP方法”还是“简单HTTP头”都与form表单支持的功能吻合。
2.需要预检的跨源请求
除了简单跨源请求之外的其他跨源请求,不需要被form表单支持,所以跨源访问时候不用再考虑对form表单做特殊地兼容。
CORS规定在实际跨源请求前,需要先执行预检请求(Preflight)。预检请求使用的是Http方法是OPTIONS,只有server端处理预检请求通过后,浏览器才可以接着发送实际请求。
插图:preflightProccess
CORS之所以使用预检请求,是因为预检请求杜绝了不安全server被跨源访问的可能性。对server端来说,一旦能正确响应预检请求,就说明server是理解CORS且针对CORS做过专门处理。如果没有preflight而是直接依靠服务器的响应来确定请求是否正确,从而使浏览器不必为单个调用发出两个请求。这样貌似简化了操作,但实际上相当于假定服务器首先正确地验证了请求,然而实际上许多服务器没有这样做,这就是CSRF攻击大行其道的原因。没有preflight请求的话就会有不安全server存在的可能性。
点击postSample按钮,查看浏览器控制台:
插图:prefight
CORS是个“圆滑”的协议,向下和向后的兼容性很好,假如有一天HTTP协议正式告别了历史遗留的FORM,那么CORS协议不需要做大改动,只需让浏览器把简单请求的概念合并为复杂请求就可以了,CORS将会进化得更加简洁和高效。
四.最佳配置实践
1.全局配置与接口单独配置
一般实现的server端技术都可以对CORS进行全局配置和每个接口单独配置。全局配置便捷方便,一次配置,所有接口都生效。每个接口单独配置则可以精确地配置每个接口的跨源访问规则。实际项目上建议采用全局配置和每个接口单独配置相结合的方式来实践。
以Java技术栈的SpringBoot+SpringSecurity框架为例。一般采用全局配置CorsConfigurationSource和接口级细粒度配置@CrossOrigin结合的方式,最终生效的属性值是全局和细粒度配置合并后的值,需要注意Spring提供的默认合并规则是“采用最大匹配进行合并”,具体可以参考:org.springframework.web.cors.CorsConfiguration类的combine(CorsConfiguration)方法,
如:
多值属性Access-Control-Allow-Headers,当全局配置CorsConfigurationSource为:
corsConfiguration.addAllowedHeader("Authorization");
而接口单独配置为:
@CrossOrigin(allowedHeaders = "*")
则最终生效的是星号“*”。
而对于单值属性Access-Control-Allow-Credentials,则以@CrossOrigin为准。
Access-Control-Allow-Credentials要尤其要注意,因为一旦开启就相当于暴露了用户相关信息,比如用户的cookie和csrf token。一般全局配置将Access-Control-Allow-Credentials设置为false。在需要跨源传递用户状态的接口单独配置为true。
2.其他
Access-Control-Allow-Origin等支持星号的响应头在生产环境不要使用星号“*”,要根据实际跨源访问需求配置真实的列表。
本位的源码上传到了Github,地址:https://github.com/andyzhaozhao/spring-security-sample-cors
技术是不断发展的,要用动态的眼光看待技术问题。能够和技术不断成长本身时间很美妙的事情
如果有任何问题和建议,可以右下角点赞后评论,我们会第一时间回复。
五.参考
更多干货都在《spring security实战》
官方资料
其他参考
- 跨域常见问题总结
- Authoritative guide to CORS (Cross-Origin Resource Sharing) for REST APIs
- 预检请求
- Spring Web MVC CORS
- Spring Boot 2中对于CORS跨域访问的快速支持
- HTTP之HEAD请求
- Jquery Ajax设置withCredentials解决跨域请求
- Web API 入门指南
- CORS:跨域资源共享 W3C的CORS Specification
- 浏览器的跨域问题以及解决方案
- Using CORS
- 跨域问题
- 怎样与 CORS 和 cookie 打交道
- CORS 进阶之 Preflight 请求
- The Web Origin Concept