之所以会出现跨域问题,主要是因为 浏览器的同源策略 所引起的,简单来说就是
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互,这是一个用于隔离潜在恶意文件的重要安全机制
- 受到同源限制
- 无法读取不同源的
Cookie
、LocalStorage
和IndexDB
- 无法获得不同源的
DOM
- 不能向不同源的服务器发送
Ajax
请求
- 无法读取不同源的
- 不受同源限制
- 在浏览器中
<script>
,<img>
,<iframe>
,<link>
等标签都可以跨域加载资源,而不受同源策略的限制
- 在浏览器中
什么是跨域
我们先来看看一个域名的组成,比如
1 | http://www.aaa.com:8080/script/index.js |
一般由 协议(http://
),子域名(www
),主域名(aaa.com
),端口号(8080
),请求资源地址(script/index.js
)组成
- 协议,网络协议遍及
OSI
通信模型(OSI 七层模型,常用协议有TCP/IP
、HTTP
、FTP
协议等) - 域名,
Domain Name
,网域,是由一串用点分隔的名字组成的Internet
上某一台计算机或计算机组的名称,用于在数据传输时标识计算机的电子方位(有时也指地理位置) - 端口,是设备与外界通讯交流的出口,分为物理端口和虚拟端口(比如常见的
80
端口)
当协议,子域名,主域名,端口号中任意一个不相同的时候,都算作不同域,不同域之间相互请求资源,就算作跨域,比如 http://www.aaa.com/index.html
请求 http://bbb.com/index.php
,JavaScript
出于安全方面的考虑,不允许跨域调用其他页面的对象,简单的理解就是因为 JavaScript
同源策略的限制,a.com
域名下的 JavaScript
无法操作 b.com
或是 c.a.com
域名下的对象
所谓同源策略,即同域名(
IP
),同端口,同协议
CORS
CORS
全称 Cross-Origin Resource Sharing
,是 W3C
的一个标准,它定义如何跨域访问资源,浏览器将 CORS
请求分成两类,简单请求(simple request
)和非简单请求(not-so-simple request
),如下
- 请求方法是以下三种方法之一,
HEAD
、GET
、POST
HTTP
的头信息不超出以下几种字段Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type
,只限于三个值application/x-www-form-urlencoded
、multipart/form-data
、text/plain
只要同时满足以下两大条件,就属于简单请求,不同时满足上面两个条件,就属于非简单请求
简单请求
浏览器会带上 Origin
的请求头发送到服务器,服务器根据 Origin
判断是否许可,如果许可就会带上 CORS
相关响应头,如果不在许可范围内就不会带上 CORS
相关的响应头,浏览器再根据响应头中是否有相关的 CORS
响应头,来判断拦截响应 body
和抛出错误
无论你是否需要用 JavaScript
通过 CORS
跨域请求资源,你都要了解 CORS
的原理,最新的浏览器全面支持,在引用外域资源时,除了 JavaScript
和 CSS
外,都要验证 CORS
,例如当你引用了某个第三方 CDN
上的字体文件时
1 | @font-face { |
如果该 CDN
服务商未正确设置 Access-Control-Allow-Origin
,那么浏览器无法加载字体资源
非简单请求
对于 PUT
、DELETE
以及其他类型如 application/json
的 POST
请求,在发送 Ajax
请求之前,浏览器会先发送一个 OPTIONS
请求(带着 Origin
、Access-Control-Request-Method
、Access-Control-Request-Headers
等 CORS
相关的请求头的预检请求)到这个 URL
上,询问目标服务器是否接受
1 | OPTIONS /path/to/resource HTTP/1.1 |
服务器必须响应并明确指出允许的 Method
1 | HTTP/1.1 200 OK |
浏览器确认服务器响应的 Access-Control-Allow-Methods
头确实包含将要发送的 Ajax
请求的 Method
,才会继续发送 Ajax
,否则抛出一个错误(可见下方实例),由于以 POST
、PUT
方式传送 JSON
格式的数据在 REST
中很常见,所以要跨域正确处理 POST
和 PUT
请求,服务器端必须正确响应 OPTIONS
请求,更多关于 CORS
的信息可以查阅 跨域资源共享 CORS 详解
处理跨域的方法
处理跨域的方法有很多,比如之前比较常见的 JSONP
,亦或者现在比较常用的 CORS
,所以我们就在这里小小的总结了一下解决跨域的相关方法,下面我们就一个一个来看
CORS
也算是目前使用较多的一种方式,针对于普通跨域请求(简单请求),只服务端设置 Access-Control-Allow-Origin
即可,前端无须设置,Origin
表示本域,也就是浏览器当前页面的域
当 JavaScript
向外域(如 sina.com
)发起请求后,浏览器收到响应后,首先检查 Access-Control-Allow-Origin
是否包含本域,如果是,则此次跨域请求成功,如果不是,则请求失败,JavaScript
将无法获取到响应的任何数据,假设本域是 my.com
,外域是 sina.com
,只要响应头 Access-Control-Allow-Origin
为 http://my.com
,或者是 *
,本次请求就可以成功,可见跨域能否成功,取决于对方服务器是否愿意给你设置一个正确的 Access-Control-Allow-Origin
,决定权始终在对方(服务器)手中
不过有一个需要注意的地方,那就是跨域请求默认不会携带 Cookie
信息,如果需要携带,可以采取如下操作
1 | 'Access-Control-Allow-Credentials': true |
或者在请求当中配置
1 | http.post(url, data, { withCredentials: true }) |
如果是非简单请求,前台则需要添加额外的 Headers
来触发非简单请求,比如下面的示例,前台采用 Ajax
1 | var xhr = new XMLHttpRequest() |
后台采用 Node.js
1 | var http = require('http') |
JSONP
JSONP
(JSON With Padding
)是 JSON
的一种使用模式,可用于解决主流浏览器的跨域数据访问的问题,主要原理是借助 <script>
等标签的 src
属性可以请求不同域名下的资源,即 <script>
请求不受浏览器同源策略影响,实现过程主要通过网页客户端动态添加 <script>
标签内的 src
属性,向服务端发送请求(不受同源策略束缚),当服务器收到请求后,将数据放在一个指定名字的回调函数里(作为参数)传回来,前台代码如下示例
1 | var script = document.createElement('script') |
上面的 <script>
标签会向本地服务器发送请求,这个请求的后面带了个 callback
参数,是用来告诉服务器回调方法的方法名的,因为服务器收到请求后,会把相应数据写进回调函数的参数位置,后端响应代码如下
1 | app.get('/jsonp', (req, res) => { |
这样浏览器通过 <script>
下载的资源就是上面的脚本了,当 <script>
下载完成就会立即执行,也就是说这个请求返回后就会立即执行上面的脚本代码,而这个脚本代码就是调用回调方法和拿到 JSON
数据,但是有一个需要注意的地方,JSONP
只支持 GET
请求方式,因为本质上 <script>
加载资源就是 GET
,但是如果我们需要发送 POST
请求那该怎么办呢?
iframe + form
如果想要发送 POST
请求,可以采用这种方式,主要原理是利用 iframe
标签的跨域能力,我们先来看看前台代码
1 | const requestPost = ({ url, data }) => { |
后台来接收并处理数据
1 | // 处理成功失败返回格式的工具 |
这样一来我们就可以发送 POST
请求了
document.domain + iframe
这种方法有些局限性,仅限主域相同,子域不同的跨域应用场景,实现原理就是让两个页面都通过 JavaScript
强制设置 document.domain
为同一域名,这样一来就实现了同域,这里有两种场景
第一种场景是在父页面调用内嵌的
iframe
当中的元素,如下- 我们的父窗口是
http://www.aaa.com/a.html
- 子窗口(内嵌的
iframe
)是http://www.aaa.com/b.html
- 我们的父窗口是
这时候如果想在 a
页面里获取 b
页面里的 DOM
进行操作,就会发现你不能获得 b
的 DOM
,比如使用 document.getElementById('myIFrame').contentWindow.document
或者 window.parent.document.body
都获取不到,都将因为两个窗口不同源而报错,在这个时候只需要在 a
页面里和 b
页面里把 document.domain
设置成相同的值就可以在两个页面里操作 DOM
了
1 | <!-- 父窗口当中内嵌子页面 --> |
- 第二种场景是共享
Cookie
引起的问题
在 a
页面里写入了 document.cookie = 'test1=hello'
,但是在 b
页面当中是获取不到这个 Cookie
的,Cookie
是服务器写入浏览器的一小段信息,只有『同源』的网页才能共享,但是两个网页一级域名相同,只是二级域名不同,在这种情况下浏览器允许通过设置 document.domain
来共享 Cookie
另外,服务器也可以在设置 Cookie
的时候,指定 Cookie
的所属域名为一级域名,这样的话,二级域名或者三级域名不用做任何设置,都可以读取这个 Cookie
,但是这里有一些需要注意的地方
document.domain
也是有限制的,虽然可读写,但只能设置成自身或者是高一级的父域且主域必须相同,所以只能解决一级域名相同二级域名不同的跨域问题document.domain
只适用于Cookie
和iframe
窗口,LocalStorage
和IndexDB
无法通过这种方法跨域
window.name + iframe
window
对象有个 name
属性,该属性有个特征,即在一个窗口(window
)的生命周期内,窗口载入的所有的页面都是共享一个 window.name
的,每个页面对 window.name
都有读写的权限,window.name
是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置,并且可以支持非常长的 name
值(2MB
),这里可以分为下面几种情况
- 第一种情况是在同一个浏览器标签页里打开了不同域名下的页面
比如先在浏览器的一个标签页里打开了 http://www.aaa.com/a.html
页面,你通过 location.href = http://www.bbb.com/b.html
在同一个浏览器标签页里打开了不同域名下的页面,这时候这两个页面可以使用 window.name
来传递参数,因为 window.name
指的是浏览器窗口的名字,只要浏览器窗口相同,那么无论在哪个网页里访问值都是一样的
- 第二种情况和上面的
document.domain + iframe
当中的第一种场景类似,但是不同之处就是两个页面的一级域名也不相同,这时候document.domain
就解决不了了
这个时候就可以使用 window.name
来解决,比如你在 b
页面里设定 window.name='hello'
,你再返回到 a
页面,在 a
页面里访问 window.name
,可以得到 hello
- 第三种情况比较少见,动态创建
iframe
,利用window.name
来传递数据,成功后再切换到同域代理页面,如下,这里分为三个页面- 父窗口,
http://www.aaa.com/a.html
- 中间代理页面,
http://www.aaa.com/proxy.html
,中间代理页,与a.html
同域,内容为空即可 - 子窗口(内嵌的
iframe
),http://www.bbb.com/b.html
(一级域名也不相同)
- 父窗口,
1 | var proxy = function (url, callback) { |
使用的话,在 b
页面当中直接设置 window.name
即可
1 | // b 页面 |
通过 iframe
的 src
属性由外域转向本地域,跨域数据即由 iframe
的 window.name
从外域传递到本地域,这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作
location.hash
location.hash
就是指 url
的 #
号后面的部分,这种情况一般使用在父窗口和 iframe
的子窗口之间通讯或者是 window.open
打开的子窗口之间的通讯,如果是两个不同域的页面 a
和 b
之间需要相互通信,则需要通过借助中间页 c
来实现,实现原理如下
1 | a.html(A 域) ==> b.html(B 域) ==> c.html(A 域) |
a
与 b
不同域只能通过 Hash
值单向通信,b
与 c
也不同域也只能单向通信,但 c
与 a
同域,所以 c
可通过 parent.parent
访问 a
页面所有对象,实例如下,三个测试页面如下
A
域下的a.html
,地址为http://www.aaa.com/a.html
B
域下的b.html
,地址为http://www.bbb.com/b.html
A
域下的c.html
,地址为http://www.ccc.com/c.html
1 | <!-- A 域下的 a.html(内嵌 B 域下的 b.html) --> |
postMessage
postMessage
是 HTML5 XMLHttpRequest Level 2
中的 API
,且是为数不多可以跨域操作的 window
属性之一,它可用于解决以下方面的问题
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的
iframe
消息传递 - 上面三个场景的跨域数据传递
语法如下
1 | window.postMessage(message, targetOrigin, [transfer]) |
有三个参数
data
,向目标窗口发送的数据,任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()
序列化origin
,协议 + 主机 + 端口号,也可以设置为*
,表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为'/'
transfer
,可选参数,是一串和message
同时传递的Transferable
对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权
另外消息的接收方必须有监听事件,否则发送消息时就会报错,如下所示
1 | The target origin provided ('http://localhost:3000') does not match the recipient window's origin ('http://localhost:3001'). |
接收消息可以直接监听 window
对象的 message
事件即可
1 | window.addEventListener('message', callback) |
callback
接收到的 message
事件包含三个属性
data
,从其他window
中传递过来的数据origin
,调用postMessage
时消息发送方窗口的origin
- 需要注意的是,这个
origin
不能保证是该窗口的当前或未来origin
- 因为
postMessage
被调用后可能被导航到不同的位置
- 需要注意的是,这个
source
,对发送消息的窗口对象的引用,可以使用此来在具有不同origin
的两个窗口之间建立双向通信
简单来说,就是一个页面发送数据,另一个页面接收数据,下面来看一个实例
1 | <!-- 父页面(内嵌一个 iframe,内容为页面 b) --> |
Nginx 代理
基本原理是我们请求的时候还是使用的前端域名,但是 Nginx
会帮我们把这个请求转发到真正的后端域名上,这样就可以避免跨域问题,Nginx
配置如下
1 | server{ |
在请求的时候,还是跟往常一样正常请求即可
1 | // 请求的时候直接使用 http://localhost:3000 |
Node.js 中间件代理跨域
中间件实现跨域代理,原理大致与 Nginx
相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置 cookieDomainRewrite
参数修改响应头中 Cookie
中域名,实现当前域的 Cookie
写入,方便接口登录认证,利用 node + express + http-proxy-middleware
搭建一个 proxy
服务器
前台代码如下
1 | var xhr = new XMLHttpRequest() |
中间件服务器代码如下
1 | var express = require('express') |
后台代码如下
1 | var http = require('http') |
如果使用的是 Webpack
构建的项目,可以使用 webpack-dev-server
代理接口跨域,在开发环境下,由于渲染服务和接口代理服务都是 webpack-dev-server
同一个,所以页面与代理接口之间不再跨域,无须设置 Headers
跨域信息了,webpack.config.js
部分配置如下
1 | module.exports = { |
WebSocket 协议跨域
WebSocket protocol
是 HTML5
一种新的协议,它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是 server push
技术的一种很好的实现,原生 WebSocket API
使用起来不太方便,我们可以选择使用 Socket.io
,它很好地封装了 WebSocket
接口,提供了更简单、灵活的接口,也对不支持 WebSocket
的浏览器提供了向下兼容
前台代码如下
1 | <div>user input:<input type="text"></div> |
后台代码如下
1 | var http = require('http') |
总结
处理跨域的方法有许多种,现在比较流行的还是使用 CORS
的方式,也就是服务端来进行设置,从而一劳永逸,不过多了解一些其他的方式也是不错的,还是那句老话,根据实际使用场景来进行选择