浏览器的跨域策略与CORS方案
本文的内容部分参考自阮一峰博客的一篇博文和MDN,
当然添加了很多补充说明,有的是来自互联网上各从业者的博客。另外我自己为了验证,做了不少测试,以及实践了服务器端的CORS改造。
不得不说PHP确实是为了Web开发设计的一门语言,在添加请求头、获取请求参数等方面使用起来非常方便。
有关跨域访问的其他解决方案可以参考阮一峰的这篇博文,有关CORS的兼容性,可以在CanIUse上看到
1. 跨域请求的默认行为
1.1. 何为跨域请求
只有协议相同、主机名相同、端口相同,才是同源。以上三者任一不同则是跨域行为。
例如:1
2
3
4http://example.com
ftp://example.com
thunder://example.com
file:///e:/example.html
这四个域名的协议不同,互相均不同源。
注意file开头的文件域名也是不能发起网络请求的,即使请求的资源也在本地也不行,所以本地测试一些静态网页的加载,需要使用服务器挂在localhost或者127.0.0.1上面。
例如:1
2
3
4
5http://exapmle.com
http://eg.com
http://test.example.com
http://example.net
http://123.123.123.123
这几个域名互相均不同源
主机名整个部分必须完全相同才是同源。即使是ip地址和域名之间也不是同源。
例如:1
2
3http://example.com
https://example.com
http://example.com:8080
这三个域名端口不同,分别是80、443、8080,因此也不是同源。
1.2. 跨域访问的限制
请求和资源方面的限制:
- 普通的XHR或Fetch无法获得预期的结果(有的浏览器无法发出请求,有的浏览器如Chrome可以发出请求但是结果会被拦截);
- Cookie、LocalStorage、IndexDB(新一代的本地存储容器)、DOM均无法访问。
不受跨域访问限制的资源:
- 加载JS和CSS文件(但是从https源站试图加载http的js文件会被一些浏览器阻止并警告,如Chrome);
- 加载img标签的图片文件;
- WebGL贴图、使用drawimage绘制canvas也可以跨域加载;
- font-face跨域加载字体也是可以的。
下图展示的是https下尝试加载http协议的js文件,这会被浏览器阻止并发出警告:
解决方法是在script中不指定加载js的协议,例如将src属性设置为//example.com/js/test.js
,没有指定协议,浏览器加载这个js文件的时候会使用当前页面所用的协议来加载。
需要额外注意的跨域问题
- 虽然js文件可以跨域加载,但是cookie是不能跨域访问的。举个例子,在example.com域名下的js代码设置的cookie只能由example.com的js代码来访问,或是被HTTP请求携带。而其他域名——例如来自eg.com域名下的js文件是无法访问到这些cookie的。
- 恶意网站可能在网页中使用iframe等方式实现向任意域名发起携带cookie的请求,这称为CSRF攻击;
- 域名和IP地址不是同源,但是部分浏览器对于DNS解析结果相同的域名和IP地址可能会当做同源。
可以发现跨域只能加载JS、CSS、图片和字体样式等,这些加载方式也常用于自CDN加载资源的场合。
常常提到的JSONP跨域方案,实际上就是将xml、json或者是其他数据转化为js,利用js可以跨域加载的原理来进行跨域访问。
2. 使用CORS来跨域共享资源
2.1. CORS的依据与实现
前面说过浏览器并不一定阻止跨域请求的发出,而只是有可能拦截请求的返回。
实际上实现CORS是在发送出跨域请求只后,浏览通过判断返回的相应的报文头信息来判断是否拦截响应的。如果返回的响应头不满足CORS条件,那么浏览器就会拦截掉该响应头。
因此,如果想实现跨域访问,服务器端需要配置额外的响应头内容,这样才能保证在跨域情况下,浏览器不会拦截响应。
对于跨域请求,浏览器依据请求的类型和头信息将其分为两种:简单请求,非简单请求。
满足以下条件的请求为简单请求:
- 类型为
GET
或HEAD
或POST
- 请求头的内容不能超出以下几个:
Accept
、Accept-Language
、Content-Language
、Last-Event-ID
、Content-Type
- 如果有Content-Type头,那么它只能包含以下几个值:
application
、x-www-form-urlencoded
、multipart/form-data
、text/plain
只要以上任一条件不满足,该请求就是非简单请求。
浏览器对于简单请求是直接发出请求,而非简单请求还要先发出一条预检请求。
2.2. 简单请求的CORS流程
发起简单请求时,浏览器会在请求头中添加一项Origin
,该项的值为发起请求的时候的完整url地址,这一项用于告知服务器跨域请求的来源。
如果服务器支持并同意该跨域请求,则浏览器的响应头会额外带有以下内容:
- 必须有
Access-Control-Allow-Origin
项,它的值必须为*
或是和请求头中的Origin相同。如果你的跨域请求还需要携带Cookie那就必须和请求头中的Origin相同,不能设为*; - 可能带有
Access-Control-Credentials
项,它的值也只能为true,表示服务器接受客户端的Cookie; - 可能带有
Access-Control-Expose-Headers
项,它是由多个字符串组成的,表示XHR能额外获取的响应头有哪些。如果服务器不给出该项,那么XHR默认只能获取以下六项响应头:Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
以及Progma
。
如果服务器不支持,或是不同意该次跨域请求,那么返回的响应头不包含Access-Control-Allow-Origin
即可
注意,即使服务器不支持跨域请求,此时响应的状态码很有可能还是200,但是在浏览器中跨域请求的相应不包含Access-Control-Allow-Origin
响应头,则会被浏览器拦截,并触发XHR的error
事件。
这里我从https://baidu.com
发起一条跨域的GET请求,请求https://a-c.fun
,可以看到浏览器给出了报错信息:
看报错信息,可以得知因为返回的响应没有包含Access-Control-Allow-Origin
所以无法获取该响应。
转到Network页面,查看发出的XHR,可以看到:
该XHR正确发出、正确响应,状态码也是200。
看看它的请求头信息:
浏览器添加了Origin
一项,值为发起请求时候所在的页面地址,因为我是在百度首页发起的所以是百度的网址,即https://www.baidu.com
。
来看一看它的响应头:
它的响应头不包含有Access-Control-Allow-Origin
内容,因此这个XHR被浏览器拦截。
2.3. 非简单请求的CORS流程
不符合简单请求的任一条件,那么该请求就是非简单请求。浏览器在进行非简单请求前,会进行一次预检请求。
预检请求的特点:
- 它的请求方法是OPTIONS;
- 浏览器额外添加
Origin
请求头,其值为发起请求的页面url; - 浏览器额外添加
Access-Control-Request-Method
请求头,表示非简单请求的类型,例如PUT
; - 还有可能添加
Access-Control-Request-Headers
请求头,表示额外携带的其他头信息,多个字符串逗号分隔。
我们做一下测试,先在百度首页发起一个CORS请求我在本地PHP的测试页面:
可以看到请求没有发送成功,转到Network页面:
可以看到这是一个预检请求,它的类型是OPTIONS。
来看看它的请求头:
可以看到多加了2个请求头,这里没有多出Access-Control-Request-Headers
是因为我没有设置额外的头数据。
如果服务器同意该次预检请求,将返回一个响应头,告知浏览器预检请求的结果。
该响应头具有以下特点:
- 必须带有
Access-Control-Allow-Origin
响应头,这是跨域访问必备的相应头信息; - 必须带有
Access-Control-Allow-Methods
响应头,它的值表示支持的请求方法,例如PUT,用逗号分隔; - 可能带有
Access-Control-Allow-Headers
响应头,表示接收浏览器发出的额外响应头信息,多个字符串用逗号分隔; - 可能带有
Access-Control-Allow-Credentials
响应头,值只能为true,表示接收浏览器发出的Cookie信息; - 可能带有
Access-Control-Max-Age
响应头,表示服务器端允许浏览器缓存该预检请求结果的时长。
可以看到只要带有前两个响应头,并且你提交的请求类型包含在Access-Control-Allow-Methods
内,那么该次预检请求就是成功的。
否则,该次预检请求即是失败的,上面那张图就是预检请求失败。
浏览器在预检请求成功的时候,才会进一步发出实际的请求,这个处理流程和简单请求类似,这里不再赘述
也就是说成功完成的非简单请求,Network中至少有2个XHR记录,其中一定有一个OPTIONS请求
3. 服务端支持CORS的改造
3.1. 简单请求服务端改造
对于简单请求,服务器端可以进行以下改造:
- 如果不要求客户端提供Cookie,那么在响应头上增加
Access-Control-Allow-Origin
项,值为*
即可; - 如果要求客户端提供Cookie,那么
Access-Control-Allow-Origin
项的值必须和Origin
请求头的值相同,服务器可以先根据Origin来判断这个跨域请求的来源是不是自己的业务自己的站点,如果安全的话将其值设置给Access-Control-Allow-Origin
响应头即可; - 如果客户端需要从响应头中获取额外的信息,还需要配置
Access-Control-Expose-Headers
项。
我们做一下CORS简单请求的服务器端改造测试。
先编写一个PHP用于响应数据,它返回字符串TEST,这是PHHP代码:
直接在浏览器中访问:
浏览器直接访问,因为直接访问是GET,肯定是可以正确获取响应的内容。
我们在百度的首页来请求这个PHP接口,因为主机名不同,所以这是一个跨域请求,无法发出:
而在本地localhost的域名上发起请求,是可以获取的:
这里用了FetchAPI,它的返回类型为Promise,通过Promise链式回调获取返回值的写法比较啰嗦。
可以看出,同样是一个简单请求,同域访问成功,跨域失败。
接下来我们尝试改造服务端,让它支持跨域请求。
首先添加Access-Control-Allow-Origin
响应头,在PHP代码中用header插入响应头:
同域请求就不用再测试了,肯定是成功的,再来测试跨域请求,还是从百度首页发起:
可以看到跨域请求成功,获取并输出了TEST字符串。
转到Network页面,查看发出的XHR:
可以看到多了一个Access-Control-Allow-Origin
响应头,因此跨域请求可以成功。
3.2. 非简单请求服务端改造
首先要处理预检请求,它的类型是OPTIONS,然后再根据具体要发起的请求进行响应。
这里在PHP中先进行判断,如果是OPTIONS的请求就给出预检成功的响应:
可以看到,添加了值为PUT的Access-Control-Allow-Methods
响应头,这表示只支持PUT类型的跨域请求,而如果不是OPTIONS预检请求,就输出字符串TEST。
在百度首页进行测试:
可以看到成功发出请求并接受到响应,转到Network栏查看具体的XHR:
第一条是OPTIONS类型的预检请求,收到了来自PHP发回的两条预检请求响应头。
因此本次请求是PUT类型,包含在Access-Control-Allow-Methods
内,本次预检成功,开始发送请求:
这次PUT请求成功发出并得到响应。
4. CORS和JSONP方式的区别
他们具有以下区别:
- JSONP只支持GET方式,而CORS支持各种类型的请求;
- JSONP支持老式浏览器,CORS并不一定保证所有老浏览器都能用,查看兼容性;
- JSONP需要服务器配置返回数据的内容,CORS需要服务器配置返回的响应头。
5. 跨域访问的其他方案
5.1. 跨域访问Cookie
如果两个域名的一级域名相同,二级域名不同,比如a.example.com
和b.example.com
两个站点之间,则可以通过修改document.domain
来互相访问Cookie,也可以只设置Cookie的一级域名。
例如,如果一个Cookie的domain属性设置为.example.com
,那么这两个站点均可以访问到该Cookie。
5.2. 跨域DOM访问
如果和上面所说的两个页面一级域名相同,那么可以使用iframe,然后更改document.domain来获取DOM。
如果一级域名也不同,也可以使用cross-document message的API来通信,可以通过url的hash值来通讯,甚至window.name来通讯。
简单介绍一下用法,首先跨文档通讯的API的兼容性很好,查看兼容性。
在用iframe加载其他页面或者windows.open( )
跨域打开页面的时候,可以获取到该页面的window
对象,在该对象上,调用.postMessage(data,origin)
方法可以发出消息:data
参数是要发出的消息,它会被序列化;origin
是一个字符串表示接收窗口的uri,目标窗口必须协议、主机名、端口号都匹配才会发出该消息。可以设置为*
表示无限制。
想要接收来自其他文档发出的跨文档消息,可以使用addEventListener('message',callback)
来接收:callback
是一个回调,它具备一个参数e;e.source
是来源文档的引用;e.origin
是来源文档的url,可以用于过滤不是发送给本文档的消息;e.data
是消息的内容,
5.3. 跨域AJAX
可以使用CORS方案、JSONP方案、Websocket方式。