AJAX与跨域通信
推荐链接:
跨站请求伪造 (缩写为 CSRF 或者 XSRF) - 维基百科
异步请求
XMLHttpRequest
AJAX
axios-js
https://github.com/axios/axios
Fetch API
https://github.com/github/fetch
AJAX 与同源策略
AJAX 解决了什么问题?
在远古时代,如果浏览器需要从服务器请求资源,其交互模式为 “客户端发出请求 -> 服务端接收请求并返回相应 HTML 文档 -> 页面刷新,客户端加载新的 HTML文档”,很显然,在这种情况下,即使只是为了更新部分数据,我们也不得不重新加载整个重绘的页面。而 AJAX 的出现解决了这个问题。
AJAX 即异步 JavaScript 和 XML,它可以在不重新加载整个网页的情况下,对网页的某部分进行异步更新。
XMLHttpRequest 对象
AJAX 的核心实现依靠的是浏览器提供的 XMLHttpRequest
对象。可以看作是一个构造函数,由此我们可以通过 const xhr = new XMLHttpRequest()
创建一个 XML 对象的实例,该实例有以下方法:
open()
:准备启动一个 AJAX 请求;setRequestHeader()
:设置请求头部信息;send()
:发送 AJAX 请求;getResponseHeader()
: 获得响应头部信息;getAllResponseHeader()
:获得一个包含所有头部信息的长字符串;abort()
:取消异步请求;
以及以下属性:
responseText
:包含响应主体返回文本;responseXML
:如果响应的内容类型是 text/xml 或 application/xml,该属性将保存包含着相应数据的 XML DOM文档;status
:响应的 HTTP 状态;statusText
:HTTP 状态的说明;readyState
:表示“请求”/“响应”过程的当前活动阶段
AJAX 请求
创建 XML 对象的实例:
1 | const xhr = new XMLHttpRequest(); |
准备请求
1 | xhr.open('get','demo.php?name=Sam&job=coder'); |
open()
方法接收三个参数:请求方式,请求 URL 地址和是否为异步请求的布尔值。
- 请求方式:有 GET 和 POST 两种,GET 请求用于向服务器拿取数据,我们可以像示例代码中那样给 URL 加上查询参数,即
?name=Sam&job=coder
,表示要查询的特定资源;POST 请求用于向服务器发送要保存的数据,数据存放的位置通过send()
方法的参数来指定。那么,对于 GET 请求,send()
方法是否可以不传递参数呢?——不可以,应该传递null
。 - 请求 URL:可以是相对路径和绝对路径
- 是否为异步请求:true 为异步,false 为同步。
设置请求头
1 | xhr.setRequestHeader('Header','Value') |
每个 HTTP 请求和响应都会带有相应的头部信息,包含一些与数据、收发者网络环境与状态等相关信息。
默认情况下,当发送 AJAX 请求时,会附带以下头部信息:
- Accept:浏览器能够处理的内容类型;
- Accept-Charset: 浏览器能够显示的字符集;
- Accept-Encoding:浏览器能够处理的压缩编码;
- Accept-Language:浏览器当前设置的语言;
- Connection:浏览器与服务器之间连接的类型;
- Cookie:当前页面设置的任何Cookie;
- Host:发出请求的页面所在的域; host、referer和origin的区别
- Referer:发出请求的页面URI;
- User-Agent:浏览器的用户代理字符串;
另外,我们还可以通过 setRequestHeader()
方法来设置请求头信息。该函数接受两个参数:头部字段(部分默认的或者自定义的)的名称和头部字段的值。
这个方法要在 open()
和 send()
之间调用
发送请求
1 | xhr.send(null) |
处理响应
目前为止,我们只是发送了请求,还没有针对服务器的响应结果做出一些处理。比方说,响应成功了怎么怎么样,响应失败了怎么怎么样。但是怎么知道是成功还是失败呢?这里就用到前面讲过的 xhr.status
属性,状态码可分为五大类:
状态码 | 分类 |
---|---|
1XX | 信息提示 |
2XX | 成功 |
3XX | 重定向 |
4XX | 客户端错误 |
5XX | 服务器错误 |
那么,根据 xhr.status
这个响应结果,我们就可以进行相应处理了:
1 | ... |
这么写对于同步请求(我们前面设置 open()
时第三个参数是 false)来说当然没问题 —— 因为是同步的,所以一定是 send 之后,服务器那边响应结果了才会继续执行后面判断 status 的代码,那么不管请求成功还是失败,这个判断一定是可以被正常执行的。但是如果是异步请求呢?对于异步请求,不需要等待服务器响应结果我们就可以执行后面的判断了,甚至可能出现一种情况是:服务器还没来得及响应结果,判断已经先执行了。那么这时候,请求一定会失败。
也就是说,我们需要加一层判断,确保收到服务器的响应结果之后,再去判断请求成功还是失败。这里就用到前面讲过的 xhr.readyState
属性,readyState 会随着 AJAX 的进程而不断变化,我们可以通过 onreadystatechange()
去监听它的变化,进而判断何时收到服务器的响应结果。
readyState 可取值有:
状态值 | 含义 | 说明 |
---|---|---|
0 | 未初始化 | 尚未调用 open() 方法 |
1 | 启动 | 已经调用 open() 方法,但尚未调用 send() 方法 |
2 | 发送 | 已经调用 send() 方法,但尚未接收到响应 |
3 | 接受 | 已经接收到部分响应数据 |
4 | 完成 | 已经接收到全部响应数据,而且已经可以在客户端使用了 |
那么,前面的代码就变成了:
1 | xhr.onreadystatechange = function(){ |
取消异步请求
设想这么一种情况:我们正在上传一张图片(也就是发送一个 AJAX 请求),由于耗时过长,我们决定取消上传,那么取消上传其实就是取消 AJAX 请求,这是通过 abort()
方法实现的。一旦调用这个方法,xhr 就会停止触发事件,而且也不再允许访问任何与响应相关的对象属性。在终止请求之后,不要忘了对 xhr 对象解引用。
正常上传:
取消上传:
XMLHttpRequest 2 级
FormData
通常提交表单数据的时候,这些数据需要经过序列化,虽然 $('#form').serialize()
可以实现序列化,但对于文件流无能为力。而 FormData
不仅可以做到表单序列化,而且支持异步上传二进制文件。
1 | var data = new FormData(); |
超时设定
为 xhr.timeout
指定一个毫秒为单位的时间,一旦浏览器在这个规定的时间内没有收到响应,就会触发 timeout
事件,执行回调函数。
1 | xhr.onreadystatechange = function () { |
注意:这时候很可能出现一种情况,就是超过1秒后浏览器没收到响应,因此终止了请求,而这时候恰好 xhr.status
为4,因此又调用函数进行判断,这个判断需要访问 xhr.status
属性,而请求已经被终止,这个属性是无法访问的,此时要用 try...catch...
捕获这个错误。
overrideMimeType() 方法
服务器返回的响应头中有一个是 Content-Type
,用以告诉客户端返回的资源类型(MIME)以及应该用什么编码去解码。例如 Content-Type:text/html;charset=UTF-8
,那么客户端就会通过 UTF-8 对资源进行解码,然后对资源进行 HTML 解析。
但可能存在一种情况:虽然服务器返回数据是 XML,但 MIME 类型指定为 text/plain
,那么这时候客户端就会当作纯文本去处理了,这显然不对,所以我们可以利用 overrideMineType()
方法重写响应的 MIME 类型,这样,客户端就可以将其当作 XML 去处理了。
1 | var xhr = new XMLHttpRequest(); |
注意,必须在 send 调用之前重写。
进度事件
Progress Events规范规范定义了与客户端与服务器通信相关的一系列事件,这些事件监听了通信进程中的各个关键节点,使我们能够以更细的颗粒度掌控数据传输过程中的细节。有以下6个进度事件:
loadstart:在接受到响应数据的第一个字节时触发
progress:在接受响应期间持续不断地触发
error:在请求错误时触发
abort:在因为调用
abort()
方法而终止连接时触发load:在接收到完整的响应数据时触发
loadend:在通信完成或触发 error、abort、load 事件后触发
每个请求都从触发 loadstart 事件开始,接下来是一或多个 progress 事件,然后触发 error、abort 或 load 中的一个,最后以触发 loadend 事件结束。
有没有发现,前面的 xhr.readyState == 4
以及这里的 load
事件都可以判断是否接受到完整响应? load
事件实际上简化了这个过程,它不需要像前者那样,既绑定一个监听函数又做一次 readyState
的判断,而只需要绑定监听函数即可。
1 | xhr.onreadystatechange = function () { |
同源策略
浏览器的同源策略即 Same-Origin Policy (SOP),它限制了不同源之间执行特定操作。
源
一个源由协议、端口、域名组成,只要有一个不同,就认为是不同源。
以 http://test.com/dist/demo.html 为例,不同源以及同源可能有以下情况:
类型 | URL | 结果 |
---|---|---|
不同协议 | https://test.com/dist/demo.html | 失败 |
不同端口 | http://test.com:80/dist/demo.html | 失败 |
不同域名 | http://test.cn/dist/demo.html 或者 http://www.test.com/dist/demo.html | 失败 |
不同路径 | http://test.com/dist2/demo.html | 成功 |
特定操作:
特定操作指的是:
- 读取 Cookie、LocalStorage、IndexDB
- 获取 DOM 元素
- 发送 AJAX 请求
为什么同源策略要禁止不同源之间进行这些操作呢?我们不妨假设一下,不存在同源策略、且不同源之间这些操作是允许的,看看可能会发生什么事。
- 我A源可以读取B源的 Cookie、LocalStorage、IndexDB,那么等于B源存储的信息都暴露了,所以同源策略禁止不同源之间读取 Cookie、LocalStorage、IndexDB;
- A源可以获取B源的 DOM 元素。那么假定用户访问了我在A源中用 iframe 引入的B源网页,他的所有操作都会在我们的掌握之中,因为我们可以在A源操作B源的 DOM 元素;
- A 源可以自由发送 AJAX 请求给B源。假定现在有一个用户首先登录了 Bank.com,那么本地客户端的 Cookie 就会记录用户在该网站的身份信息,之后用户不小心点进了危险网站 Evil.com,这个网站做了一些设置,一旦用户进入,就自动发送 AJAX 请求给 Bank.com,由于发送请求的时候,浏览器会自动在本地检索目标网站的 Cookie ,并添加到请求报文中,所以此时目标网站的 Cookie 被请求携带着发送过去了,而 Bank.com 的响应头又是携带着 Cookie 返回的,那么这时候等于 Evil.com 已经拿到了这个 Cookie。也就是说,发送请求前它确实拿不到这个 Cookie(是浏览器给请求报文加上的,不是我们),但接受到响应后它的的确确拿到了,于是事情一发不可收拾……
跨域通信
这样看来,同源策略确实很有存在的必要,不然网络安全无从谈起。等等,既然有同源策略的限制,那我A域怎么去请求B域中的资源呢?也就是说,要怎么解决跨域通信的问题呢?
跨域解决方案
常见的几种跨域方案:
JSONP
、CORS
、图像Ping
、document.domain
、window.name
;
postMessage
、location.hash
、WebSocket
、Nginx 反向代理
、Nodejs 中间件代理
;
开始之前,要先清楚一件事:
跨域不一定是浏览器限制了发起跨站请求,也可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了。最好的例子是 CSRF 跨站攻击原理,请求是发送到了后端服务器无论是否跨域!注意:有些浏览器不允许从 HTTPS 的域跨域访问 HTTP,比如 Chrome 和 Firefox,这些浏览器在请求还未发出的时候就会拦截请求,这是一个特例。
JSONP
<link>
获取 CSS,<script>
获取 JS,<img>
获取图片,这些明明也是跨域获取资源,为什么不会被禁止呢?很简单,因为这些都不属于上述特定操作之一,这里请求资源压根没用到 AJAX 请求。再看看我们的需求,我们现在是要在 A 域中获取 B 域资源,那么我完全可以在 A 域中动态创建一个 script
并请求 B 域资源,然后,因为 A 域中的 js 和 scirpt
中的 js 是在同一个作用域中的,所以要在 A 域中展示 B 域的数据也完全不成问题。虽然说法比较简陋,但这就是 JSONP 的原理。下面我们来看看具体实现:
1 | // 1.回调函数 |
首先是客户端的角度,这段代码声明了一个用以接受数据的回调函数,之后动态创建了 script
,执行完毕之后来到 body
,这时候遇到语句 <script src='http://test.com/json?callback=handleResponse'></script>
,此时会向服务器发起一次资源请求;然后来到服务端的角度,服务端解析上述的 url,得到查询参数 callback 的值是 handleResponse,此时会生成一个对应的函数执行语句,也就是 handleResponse(data)
,这个语句返回给了客户端这边,客户端执行该语句(因为当前作用域确实声明了这个 handleResponse 函数),打印相关数据。这样就算完成一次跨域请求了。
JSONP 使用起来虽然很简单,但是有如下缺点:
- 无法发送 POST 请求
- 安全问题。万一服务端那边夹带恶意代码返回过来,那么客户端这边是会直接执行的,因此有安全隐患
- 无法监测 JSONP 请求是否成功或失败
CORS
CORS 即 Cross-origin resource sharing,跨域资源共享 ,是由 W3C 官方推广的允许通过 AJAX 技术跨域获取资源的规范 。
CORS 的关键在于服务端,也就是客户端这边发送请求,服务端那边做一些判断(请求方是否在自己的“白名单”里?),如果没问题就返回数据,否则拒绝。
浏览器将 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
其中一种)
凡不同时满足以上两大条件的,都属于非简单请求。
下面我们看一下针对这两种请求,CORS 是怎么处理。
简单请求
首先是客户端的角度,发送请求时浏览器检测到这是一个简单请求,因此在请求头额外增加一个 Origin
,它的值是请求代码所在的源,例如 http://test.com
:
1 | GET /cors HTTP/1.1 |
然后是服务端的角度,服务端收到请求,首先检测请求报头的 Origin
是否在自己的许可范围内,
如果确实是许可的域,那么待会响应的时候,响应头会额外增加如下字段:
- Access-Control-Allow-Origin(必选) :这个字段用来告知客户端,服务端能够接受的发送 AJAX 请求的域,因为此次请求得到许可,所以这里返回与先前请求报头中
Origin
匹配的http://test.com
。当然,也可以返回*
,表示接受任何域的 AJAX 请求(*
是通配的意思)。 - Access-Control-Allow-Credentials (可选):告知浏览器,是否允许客户端发送请求的时候携带 Cookie,true 表示允许,false 表示禁止,出于安全问题考虑(前面说过),CORS 默认不允许跨域 AJAX 请求携带 Cookie。
- Access-Control-Expose-Headers(可选):该字段用来向客户端暴露可获取的响应头。默认情况下,xhr 的
getResponseHeader()
方法只能拿到 6 个基本响应头字段,如果还想额外拿到其它字段,那么前端要和后端商量好,让后端在Access-Control-Expose-Headers
指定好前端可以通过该方法获取的额外响应头字段。
如果不是许可的域,那么这时候其实压根不会返回 Access-Control-Allow-Origin
这个响应头,而浏览器会捕获这次错误,如下图所示:
PS:虽然禁止跨域 AJAX 请求携带 Cookie 是为了安全考虑,但由于它在身份验证中的重要性,我们有时候还是得携带 Cookie 的。 具体方法是:
- 客户端配置
withCredentials
属性:
1 | var xhr = new XMLHttpRequest() |
- 服务端配置
Access-Control-Allow-Credential
为 true,配置Access-Control-Allow-Origin
为指定的域(而不是*
),
非简单请求
非简单请求包括两次请求,第一次请求是 preflight request,也就是预检/查询请求,这次请求试探性地“询问”服务端,自己打算进行的非简单请求是否合法 —— 不管是否合法,服务端都会通过某种方式通知客户端,客户端基于这个结果,判断是否进行第二次真正的请求。
预检请求是这样的:
首先是客户端的角度,发送请求时浏览器检测到这是一个非简单请求,所以事先向服务端发送一个预检请求:
1 | OPTIONS /cors HTTP/1.1 |
- 注意,这里这个预检请求的类型是 OPTIONS 。
- 像之前的简单请求一样,这里浏览器会追加一个
Origin
,表示请求代码所在的源 - 前面我们说过,非简单请求会多出额外的请求头字段,这里多出来的就是
Access-Control-Request-Method
和Access-Control-Request-Headers
,这其实是告诉服务端,“我待会要进行的真正请求,类型是这里Access-Control-Request-Headers
指定的类型,然后自定义请求头是这里Access-Control-Request-Headers
指定的值,你看看行不行,给我个回应“。
好了,我们来看看服务器作何反应。来到服务端的角度,服务端收到这个请求,它会检测请求头中的信息,发现这个请求是合法的、没啥毛病,“好,我同意你的第二次请求”,不过光说不行,得在返回的响应头中告诉客户端这一点,此时响应头是这样的:
1 | HTTP/1.1 200 OK |
- Access-Control-Allow-Origin:这里和之前一样,可以是 http://test.com 或者
*
,也就是告诉客户端,“我给你的域下了许可证“ - Access-Control-Allow-Methods:这里告诉客户端,服务端允许的跨域 AJAX 请求的类型,”虽然你刚才告诉我你准备进行的是 PUT 请求,不过你要进行 GET 或者 POST 请求,我也是允许的“
- Access-Control-Allow-Headers:这里告诉客户端,服务端允许的发送请求时的自定义请求头
- Access-Control-Max-Age: 这里告诉客户端预检请求的有效期,省去了多次的预检请求。也就是说,”我给你开个后门,1728000 秒内(20天内)你可以直接发送真正的 AJAX 请求,不用每次都来问我了“
再回到客户端这边,客户端收到响应,知道服务端允许了自己的请求,于是进行第二次真正的 AJAX 跨域请求。此后每次 CORS 请求 都相当于一次简单请求了。
但是,如果发现客户端请求不合法,那么服务端虽然会返回正常响应,但不会返回 CORS 相关的响应头,而客户端这边”心领神会“,知道被拒绝了,所以由 xhr 对象捕获这个错误,如下图所示:
我们可以来解读一下这个报错:上图的 Response to preflight request 就是服务端对于预检请求的响应,这个响应返回到客户端之后,客户端进行一次 access control check,也就是检查这个响应是否有标志着服务端同意的响应头,因为 No ‘Access-Control-Allow-Origin’ header is present on the requested resource,也就是说我客户端这边并没有检查到服务端本应提供的 Access-Control-Allow-Origin 响应头,所以最终 doesn’t pass access control check,也就是没有通过这次检查。
图像 Ping
- 图像 Ping 是与服务器进行简单、单向的跨域通信的一种方式,请求的数据是通过查询字符串形式发送的,而响应可以是任意内容,通常是像素图和 204 响应。浏览器虽然得不到任何具体数据,但由于可以监听 load 和 error 事件,所以能知道响应是什么时候接受到的。
- 图像 Ping 最常用于跟踪用户点击页面或动态广告曝光次数
- 缺点:单向通信,只支持 GET 请求;无法访问服务器的响应文本
document.domain
介绍 document.domain 跨域之前,先解释一下域名的一些概念。
- 顶级域名:诸如 .com、.cn、.net、.org 等都是顶级域名,也叫一级域名
- 二级域名:诸如 baidu.com、zhihu.com、mdn.org 等
- 父域名、子域名:这是相对的概念,诸如 .com 是 tool.com 的父域名,而 tool.com 的子域名是 editor.tool.com,editor.tool.com 的子域名是 www.editor.tool.com
document.domain 适用于主域相同、子域不同的两个域之间的跨域通信。假设我现在有一个A域为 http://www.test.com/a.html ,另一个B域为 http://test.com/b.html ,因为是不同源的(域名不相同),所以我不能在A域中拿到B域的东西,但是呢,我们注意到这两个域的主域是相同的,只是子域不同而已,所以我们可以用 document.domain 的方法实现跨域,具体来说,就是重新设置两个页面的 document.domain 为一个相同的值。
但要注意的是,document.domain 的设置是有限制的,我们只能把 document.domain 设置成自身或更高一级的父域,且主域必须始终保持相同。例如:a.b.test.com 中某个文档的 document.domain 可以设成a.b.test.com(自身)、b.test.com(上一级父域) 、test.com(上上一级父域)中的任意一个,但是不可以设成 c.a.b.test.com(下一级子域),因为这是当前域的子域,也不可以设成 baidu.com,因为主域已经不相同了,这里的主域必须始终保持为 test.com 不变。
来看代码:
A域 http://www.test.com/a.html :
1 | <iframe src=" http://test.com/b.html" id="myIframe" onload="test()"> |
1 | <script> |
之后,我们就可以在 A 域中拿到 B 域的东西了。注意,尽管这时候 document.domain 是一样的,但两个域之间只是可以交互而已,仍然不能发送 AJAX 请求。
window.name
首先要明白一件事 —— window 对象有个 name 属性,在一个窗口的生命周期内,window.name
会被该窗口的所有页面所共享、所读写,不管这些页面是同源还是不同源。
那么,我们岂不是可以把数据放在 window.name
里,然后通过页面跳转把这些数据拿到自己这边来?有道理,不过每次要拿数据就得跳转页面,好像有点麻烦,不妨我们把这个页面跳转的过程放在 iframe
里进行。假定请求数据的页面是 a.html,存放数据的页面是 c.html,那么我们在 a.html 中通过 iframe
加载 c.html,这时候数据已经存放在 iframe
这个窗口的 window.name
里了,之后我们让其跳转到与 a.html 同源的 b.html,根据前面说的,window.name
仍然是被保留的、可访问的,那么 window.name
由 c 传递到了 b,并且由于此时 a.html、b.html 同源,所以 window.name
又可经由 b 传递给 a。
下面说说代码实现:
1 | // c.html |
1 | // b.html |
1 | // a.html |
这里动态创建了 iframe
,并指定第一次加载的 iframe
是 c.html,一旦加载好(很显然这时候 window.name
的值已经记录在这个窗口里了),就执行回调函数,通过修改 src 让页面跳转到 b.html(这时候 window.name
的值传递给了 b.html),第二次触发执行回调函数,将最初的数据传递给 a.html。
注意两个地方:
- 由于整个过程是悄悄进行的,我们给
iframe
设置display:none
- 拿到数据后记得销毁
iframe
,防止内存泄露
上面的写法不需要重写 onload 回调函数,只用一个 flag 标识第一和第二次加载;我们也可以采用下面的方法重写 onload 回调:
1 | iframe.onload = function () { |
postMessage
HTML5 提供了 postMessage
和 onmessage
两个 api 用于在跨域站点页面之间进行通信。
假设A域要向B域发送消息,那么:
- 一方面,我们在A域页面中通过
iframe
引入B域,之后用B域的 window 对象调用postMessage
方法(谁接受消息,谁就去调用)。这个方法接受两个参数,第一个参数是发送的消息, 它可以是任何类型的数据,但部分浏览器只支持字符串格式;第二个参数是可以接受消息的域,如果不想限定某个域(比如B)去接受消息,那么可以传*
。 - 另一方面,B域监听
message
事件,一旦接收到消息就调用某个函数接受数据。message
事件的事件对象有三个属性,event.data
表示接受到的数据,event.origin
为消息发送方的源,event.source
为消息发送方的窗口对象的引用。
B域接收到了消息,要通知A域,其实就是上面的过程反过来。
B域要向A域发送消息,那么:
- 一方面,B 域的
window.parent
可以访问父级(A域)窗口对象,我们在B域里通过该对象调用postMessage
方法,发送通知给A域 - 另一方面,A域监听
message
事件,收到B域通知,进行相应处理
核心代码如下:
1 | http://test.com/a.html |
1 | http://anothertest.com/b.html |
那么这就是简单的跨域窗口间通信了,不过这只是客户端层面上的,如果A域的客户端要发送 AJAX 请求给B域服务端呢?只要稍微改进上面的方法就可以,也就是说,B域客户端充当一个中转站,A 域客户端先通过上面的方法把数据发送给B域客户端,B域客户端再把数据转发给B域服务端(这两个是同源的,直接发送 AJAX 请求);然后,反过来也一样,B域返回的数据经由B域客户端交给A域客户端。
代码如下:
1 | http://anothertest.com/b.html |
另外还要关注安全问题。 postMessage 本质上是依赖于客户端脚本设置了相应的 message
监听事件,因此只要有消息通过postMessage
发送过来,我们的脚本都会接收并进行处理 —— 而任何域都可以通过 postMessage
发送跨域信息,因此对于设置了事件监听器的页面来说,判断到达页面的信息是否安全是非常重要的。通常可以通过 event.origin
检测消息方是否在消息源白名单中。
location.hash
默认情况下,改变页面的 url 会导致页面跳转,但是 hash 是个例外,譬如将 http://test.com/a.html#hash 改为 http://test.com/a.html#anotherhash ,并不会引起页面跳转,所以我们可以利用 hash 来传输数据。
假设A域有 a.html 和 b.html,B域有 c.html,且 a.html 和 c.html 之间要进行跨域通信。
- 一方面,我们在 a.html 中通过
iframe
引入 c.html,引用的 src 带上 hash —— 实际上这时候已经通过 hash 的方式把数据传给 c.html 了 - 另一方面,在 c.html 中,我们对这个数据进行一些处理,之后想办法返回给 a.html。怎么返回呢?假定 a、c 同域,那么可以通过将数据赋值给
window.parent.location.hash
的方式,让 a.html 的 hash 改变,同时 a.html 监听这个改变,保存传过来的数据。但问题是,a、c 是不同源的,我们无法在 c.html 中通过window.parent
去访问 a.html。那么谁能和 a.html 直接通信呢?肯定是和 a.html 同源的 html,因此我们想到,在 c.html 中利用iframe
引入与 a.html 同源的 b.html,引用的 src 带上 hash —— 实际上这时候已经通过 hash 的方式把数据传给 b.html 了,而 b.html 拿到数据后,由于它和 a.html 是同源的,所以可以直接将数据赋值给window.parent.parent.location.hash
,之后,a.html 监听 hash 改变,保存数据。
如下图所示:
下面我们看一下代码是怎么写的。
像前面说的,我们创建 iframe
引用 c.html,通过 hash 传值,同时监听 a.html 的 hash 改变 —— 这里有两种方式,我们可以直接用 onhashchange 监听,也可以设置一个定时器,每隔两秒轮询一次 hash,一旦改变就打印数据。
1 | // a.html |
这里我们根据不同的参数采取不同的处理,因为传过来的是 #getdata
,所以调用 callBack
函数,函数首先尝试直接用 parent.location.hash
改变 a.html 的 hash,发现是不同源的,更改失败,改为将数据传给 b.html。
1 | // c.html |
由于 b.html 和 a.html 同源,所以可以直接更改 a.html 的 hash。更改后触发 a.html 中的事件,打印数据。
1 | // b.html |
location.hash 跨域的大致过程就是这样,当然,它的缺点也很明显:
- 数据直接暴露在了 url 中
- 数据容量和类型有限
WebSocket
https://www.runoob.com/html/html5-websocket.html
传统的 http 协议有一个缺陷:通信只能由客户端发起,服务端无法主动向客户端推送信息。比如,服务端这边某个状态发生变化,它是无法主动通知客户端的,而只能由客户端采用轮询的方式,每隔一段时间发送一次请求进行探测。
这时候出现了一种新的叫做 WebSocket 的协议,它使用ws://
(非加密)和 wss://
(加密)作为协议前缀,特点在于支持双向通信 —— 客户端可以主动向服务端发送信息,服务端也可以主动向客户端推送信息 。那么这和跨域有什么关系呢?事实上,WebSocket 本身就不受同源策略的影响,这意味着,一旦客户端与服务端建立的是 WebSocket 连接,天然就可以实现跨域资源共享。
建立 WebSocket 连接
客户端要求升级至 WebSocket 协议:
1 | GET /chat HTTP/1.1 |
服务端同意升级:
1 | HTTP/1.1 101 Switching Protocols |
发起请求
同源策略限制了不同源之间无法发送 AJAX 请求,但是 WebSocket 发送的并不是 AJAX 请求,而是 WebSocket 请求。在了解怎么发起 ws 请求之前,先看一下一些相关属性。
WebSocket
对象的readyState
属性用来表示对象实例当前所处的连接状态,有四个值:- 0:表示正在连接中(CONNECTING);
- 1:表示连接成功,可以通信(OPEN);
- 2:表示连接正在关闭(CLOSING);
- 3:表示连接已经关闭或打开连接失败(CLOSED);
- 另外还有四个事件属性:
onopen
:用于指定连接成功后的回调函数;onclose
:用于指定连接关闭后的回调函数;onmessage
:用于指定收到服务器数据后的回调函数;onerror
:用于指定报错时的回调函数;
- 另外还提供了
bufferedAmount
属性,表示还剩下多少字节的二进制数据没有发送出去
1 | let ws = new WebSocket('ws://localhost:3000'); |
不过, 原生的 WebSocket API 使用起来不太方便,可以使用 Socket.io,它很好地封装了 WebSocket 接口,提供了更简单、灵活的接口,也对不支持 WebSocket 的浏览器提供了向下兼容。
Nginx 反向代理
Nodejs 中间件代理
原理和 nginx 相同,通过代理服务器,实现数据的转发 。