本文的内容部分参考自阮一峰博客的一篇博文MDN
当然添加了很多补充说明,有的是来自互联网上各从业者的博客。另外我自己为了验证,做了不少测试,以及实践了服务器端的CORS改造。
不得不说PHP确实是为了Web开发设计的一门语言,在添加请求头、获取请求参数等方面使用起来非常方便。
有关跨域访问的其他解决方案可以参考阮一峰的这篇博文,有关CORS的兼容性,可以在CanIUse上看到

1. 跨域请求的默认行为

1.1. 何为跨域请求

只有协议相同、主机名相同、端口相同,才是同源。以上三者任一不同则是跨域行为。
例如:

1
2
3
4
http://example.com
ftp://example.com
thunder://example.com
file:///e:/example.html

这四个域名的协议不同,互相均不同源。
注意file开头的文件域名也是不能发起网络请求的,即使请求的资源也在本地也不行,所以本地测试一些静态网页的加载,需要使用服务器挂在localhost或者127.0.0.1上面。

例如:

1
2
3
4
5
http://exapmle.com
http://eg.com
http://test.example.com
http://example.net
http://123.123.123.123

这几个域名互相均不同源
主机名整个部分必须完全相同才是同源。即使是ip地址和域名之间也不是同源。

例如:

1
2
3
http://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条件,那么浏览器就会拦截掉该响应头。
因此,如果想实现跨域访问,服务器端需要配置额外的响应头内容,这样才能保证在跨域情况下,浏览器不会拦截响应。

对于跨域请求,浏览器依据请求的类型和头信息将其分为两种:简单请求,非简单请求
满足以下条件的请求为简单请求

  • 类型为GETHEADPOST
  • 请求头的内容不能超出以下几个:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type
  • 如果有Content-Type头,那么它只能包含以下几个值:applicationx-www-form-urlencodedmultipart/form-datatext/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-ControlContent-LanguageContent-TypeExpiresLast-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.comb.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方式。