JobbyM's Blog

一日一练-JS 了解几种跨域技术

子曰:了解几种跨域机制

简单介绍

首先简单了解一下同源策略相关知识点:
1.同源策略 限制了从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要机制。
2.源的定义:如果两个页面的协议、端口和域名都相同,则两个页面具有相同的
3.同源策略规定,是XHR 实现Ajax 通信的一个主要限制。默认情况下,XHR 对象只能访问与包含它的页面位于同一个域中的资源。这种安全策略可以预防某些恶意行为。但是,实现合理的跨域请求对开发某些浏览器应用程序也是至关重要的。

下面是几种跨域技术。

CORS

0x00:定义
CORS(Cross-Origin Resource Sharing,跨域资源共享)是W3C 的一个工作草案,定义了在必须访问跨域资源时,浏览器与服务器应该如何沟通。CORS 背后的基本思想,就是使用自定义的HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。
比如一个简单的使用GET 或POST 发送的请求,它没有自定义的头部,而主体内容是text/plain。在发送该请求时,需要给它附加应该额外的Origin 头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给与响应。下面是Origin 头部的应该示例:

1
Origin: http://www.example.com

如果服务器任务这个请求可以接受,就在Access-Control-Allow-Origin 头部中回发相同的源信息(如果是公共资源,可以回发*)。例如:

1
Access-Control-Allow-Origin: http://www.example.com

如果没有这个头部,或者有这个头部但源信息不匹配,浏览器就会驳回请求。正常情况下,浏览器会处理请求。注意,请求和响应都不包含cookie 信息。

0x01:现代浏览器对CORS 的实现
Webkit 内核的现代浏览器都通过XMLHttpRequest 对象实现了对CORS 的原生支持。在尝试打开不同来源的资源时,无需额外编写代码就可以触发这个行为。

1
2
3
4
5
6
7
8
9
10
11
12
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText)
} else {
alert('Request was unsuccessful: ' + xhr.status)
}
}
}
xhr.open('get', 'http://www.example.com/page/', true)
xhr.send(null)

跨域XHR 对象有一些安全限制
1.不能使用setRequestHeader() 设置自定义头部。
2.不能发送和接收cookie
3.调用getAllResponseHeader() 方法总会返回空字符串。

0x02:Preflighted Requests(预检请求)
CORS 通过一种叫做Preflighted Requests 的透明服务器机制支持开发人员使用自定义的头部、GET 或POST 之外的方法,以及不同类型的主体内容。在使用下列高级选项发送请求时,就会向服务器发送一个Preflight 请求。这种请求使用OPTIONS 方法,发送下列头部。
1.Origin:与简单的请求相同。
2.Access-Control-Request-Method:请求自身使用的方法。
3.Access-Control-Request-Headers:(可选)自定义的头部信息,多个头部以逗号分割。
下面是一个带有自定义头部NCZ 的使用POST 发送的请求

1
2
3
Origin: http://www.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ

发送这个请求后,服务器可以决定是否允许这种类型的请求。服务器通过在响应中发送如下头部与浏览器进行沟通。
1.Access-Control-Allow-Origin:与简单的请求相同。
2.Access-Control-Allow-Methods:允许的方法,多个方法以逗号分隔。
3.Access-Control-Allow-Headers:允许的头部,多个头部以逗号分隔。
4.Access-Control-Max-Age:应该将这个Preflight 请求缓存多长时间(以秒表示)。
例如:

1
2
3
4
Access-Control-Allow-Origin: http://www.example.com
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Headers: NCZ
Access-Control-Mag-Age:1728000

Preflight 请求结束后,结果将按照响应中指定的时间缓存起来。而为此付出的代价只是第一次发送这种请求时会多一次HTTP 请求。

0x03:带凭据的请求
默认情况下,跨源请求不提供凭据(cookie,HTTP 认证及客户端SSL 证明等)。通过将withCredentials 属性设置为true,可以指定某个请求应该发送凭据。如果服务器接受带凭据的请求,会用下面的HTTP 头部来响应。

1
Access-Contol-Allow-Credentials: true

如果发送的是带凭据的请求,但服务器的响应中没有包含这个头部,那么浏览器就不会把响应交给JavaScript(于是,responseText 中将是空字符串,status 的值为0,而且会调用onerror() 事件处理程序)。另外,服务器还可以在Preflight 响应中发送这个HTTP 头部,表示允许源发送带凭据的请求。

图像Ping

0x00:定义
图像Ping 是与服务器进行简单、单向的跨域通信的一种方式。请求的数据是通过查询字符串形式发送的,而响应可以是任意内容,但通常是像素图或204 响应。通过图像Ping,浏览器得不到任何具体的数据,但通过侦听load 和error 事件,它能知道响应是什么时候收到。

0x01:例子

1
2
3
4
5
var img = new Image()
img.onload = img.onerror = function () {
alert('Done!')
}
img.src = 'http://www.example.com/test?name=Nicholas'

这里创建了一个Image 实例,然后将onload 和onerror 事件处理程序指定为同一个函数。这样无论是什么响应,只要请求完成,就能得到通知。请求从设置src 属性那一刻开始,而这个例子在请求中发送了一个name 参数。

0x02: 作用
图像Ping 最常用于跟踪用户点击页面或动态广告曝光次数。图像Ping 有两个主要的缺点,一是只能发送GET 请求,而是无法访问服务器的响应文本。因此,图像Ping 只能用于浏览器与服务器间的简单通信。

JSONP

0x00:定义
JSONP 是JSON with Padding(填充式JSON或参数式JSON)的简写,是应用JSON 的一种新方法。JSONP 看起来与JSON 差不多,只不过是被包含在函数调用中的JSON。

1
callback({"name": "Nicholas"})

JSONP 由两部分组成:回调函数和数据。回调函数是当响应到来时应该在页面中调用的函数。回调函数的名字一般是在请求中指定的。而数据就是传入回调函数中的JSON 数据。

0x01:例子
这是应该典型的JSONP 请求

1
http://freegeoio.net/json/?callback=handleResponse

这个URL 是在请求应该JSONP 地理定位服务。通过查询字符串来指定JSONP 服务的回调参数,这里指定的回调函数的名字叫handleResponse()
JSONP 是通过动态创建<script> 元素来使用的,使用时可以为src 属性指定应该跨域URL。<script> 元素和<img> 元素都有能力不受限制地从其他域加载资源。因为JSONP 是有效的JavaScript 代码,所以在请求完成后,即在JSONP 响应加载到页面中以后,就会立即执行。

1
2
3
4
5
6
7
function handleResponse (response) {
alert("You're at IP address " + reponse.ip + ", which is in " +
response.city + ", " + response.region_name)
}
var script = document.createElement('script')
script.src = 'http://freegeoio.net/json/?callback=handleResponse'
document.body.insertBefore(script, document.body.firstChild)

0x02: 作用
与图像Ping 相比,优点在于能够直接访问响应文本,支持在浏览器与服务器之间双向通信。
缺点:需要确保其他域的安全可靠,以及确定JSONP 请求是否失败并不容易。

Comet

0x00:定义
Comet 指的一种更高级的Ajax 技术(也被称为“服务器推送”)。Ajax 是一种从页面向服务器请求数据的技术,而Comet 则是一种服务器向页面推送数据的技术。Comet 能够让信息近乎实时地被推送到页面上,非常适合处理体育比赛的分数和股票报价。

0x01:实现
有两种实现Comet 的方式:长轮询和流。
1.短轮询:浏览器定时向服务器发送请求,看看有没有更新的数据。下图是短轮询的时间线

2.长轮询:页面发生器一个服务器的请求,然后服务器一直保持连接打开知道有数据可发送。发送完数据之后,浏览器关闭连接,随即由发起一个到服务器的新请求。这一过程在页面打开期间一直保持不断。下面是长轮询的时间线

3.HTTP流:流不同于轮询,因为它在页面的整个生命周期内只使用一个HTTP 连接。具体来说,就是浏览器向服务器发送一个请求,而服务器保持连接打开,然后周期性地向浏览器发送数据。
所有服务器端语言都支持打印到输出缓存然后刷新(将输出缓存中的内容一次性全部发送到客户端)的功能。而这正是实现HTTP 流的关键所在。

在现代浏览器中,通过侦听readstatechange 事件以及检测readyState 的值是否为3,就可以利用XHR 对象实现HTTP 流。随着不断从服务器接收数据,readState 的值就会周期性地变为3。当readyState 值变为3 时,responseText 属性中会保存接收到的所有数据。

服务器发送事件

0x00:定义
SSE(Server-Sent Events,服务器发送事件)是围绕只读Comet 交互推出的API 或者模式。SSE API 用于创建到服务器的单向连接,服务器通过这个连接可以发送任意数量的数据。服务器响应的MIME 类型必须是text/event-stream,而且是浏览器中的JavaScript API能解析格式输出。

0x01: SSE API
SSE 的JavaScript API 与其他传递消息的JavaScript API 很相似。要预定新的事件流,首先要创建一个新的EventSource 对象,并传进一个入口点:

1
var source = new EventSource('myevents.php')

注意,传入的URL 必须与创建对象的页面同源(相同的URL 模式、域及端口)。EventSource 的实例有一个readyState 属性,值为0 表示正连接到服务器,值为1 表示打开连接,值为2 表示关闭连接。
另外,还有以下三个事件。
1.open:在建立连接时触发。
2.message:在从服务器接收到新事件时触发。
3.error:无法建立连接时触发。

onmessage 事件处理程序的使用

1
2
3
4
source.onmessage = function (event) {
var data = event.data
// 处理数据
}

服务器发回的数据以字符串形式保存在event.data 中。
默认情况下,EventSource 对象会保持与服务器的活动连接。如果连接断开,还会重新连接。这就意味着SSE 适合长轮询和HTTP 流。如果想强制立即断开连接并且不再重新连接,可以调用close() 方法。

1
source.close()

0x02:事件流
所谓的服务器事件会通过一个持久的HTTP 响应发送,这个响应的MIME 类型为text/event-stream。响应的格式是纯文本,最简单的情况是每个数据项都带有前缀data:,例如:

1
2
3
4
5
6
data: foo

data: bar

data: foo
data: bar

对以上响应而言,事件流中的第一个message 事件返回的event.data 值为foo,第二个message 事件返回的event.data 值为bar,第三个message 事件返回的event.data 值为foo\nbar(注意中间的换行符)。对于多个连续的以data: 开头的数据行,将作为多段数据解析,每个值之间以一个换行符分割。只能在包含data:的数据行后面有空行时,才会触发message 事件,因此在服务器上生成事件流时不能忘了多添加这一行。
通过id: 前缀可以给特定的事件指定一个关联的ID,这个ID 行位于data: 行前面或皆可:

1
2
data: foo
id: 1

设置了ID 后,EventSource 对象会跟踪上一次触发的事件。如果连接断开,会向服务器发送一个包含名为Last-Event-ID 的特殊HTTP 头部的请求,以便服务器知道下一次该触发哪个事件。在多次连接的事件流中,这种机制可以确保浏览器以正确的顺序收到连接的数据段。

Web Sockets

0x00:定义
Web Sockets 的目标是在一个单独的持久连接上提供全双工、双向通信。在JavaScript 中创建了Web Socket 之后,会有一个HTTP 请求发送到浏览器以发起连接。在取得服务器响应后,建立的连接会使用HTTP 升级为HTTP 协议交换为Web Socket 协议。也就是说,使用标准的HTTP 服务器无法实现Web Sockets,只有支持这种协议的专门服务器才能正常工作。
由于Web Sockets 使用了自定义的协议,所以URL 模式也略有不同。未加密的连接不再是http:// 而是ws://;加密的连接也不是https://而是wss://
使用自定义协议而非HTTP 协议的好处是,能够在客户端和服务器之间发送非常少量的数据,由于传递的数据包很小,因此Web Sockets 非常适合移动应用。

0x01:Web Sockets API
要创建Web Socket,先实例一个WebSocket 对象并传入要连接的URL:

1
var socket = new WebSocket('ws://www.example.com/server.php')

注意,必须给WebSocket 构造函数传入绝对URL。同源策略对Web Sockets 不适用,因此可以通过它打开到任何站点的连接。

实例化了WebSocket 对象后,浏览器就会马上尝试连接。WebSocket 也有一个表示当前状态的readyState 属性。
1.WebSocket.OPENING(0):正在建立连接。
2.WebSocket.OPEN(1):已经建立连接。
3.WebSocket.CLOSING(2):正在关闭连接。
4.WebSocket.CLOSE(3):已经关闭连接。
readyState 的值永远从0 开始。
要关闭Web Socket 连接,可以在任何时候调用close() 方法。

1
socket.close()

调用了close() 之后,readyState 的值立即变为2(正在关闭),而在关闭连接后就会变成3。

0x02:发送和接收数据
Web Socket 打开之后,就可以通过连接发送和接收数据。要向服务器发送数据,使用send() 方法并传入任意字符串,例如:

1
2
var socket = new WebSocket('ws://www.example.com/server.php')
socket.send('Hello world!')

因为Web Socket 只能通过连接发送纯文本数据,所以对于复杂的数据结构,在通过连接发送之前,必须进行序列化。下面的例子展示了先将数据序列化为一个JSON 字符串,然后再发送到服务器:

1
2
3
4
5
6
var message = {
time: new Date(),
text: 'Hello world!',
clientId: 'asdfp'
}
socket.send(JSON.stringify(message))

接下来,服务器要读取其中的数据,就要解析接收到的JSON 字符串。
当服务器向客户端发来消息时,WebSocket 对象就会触发message 事件。这个message 事件与其他传递消息的协议类似,也是把返回的数据保存在event.data 属性中。

1
2
3
4
socket.onmessage = function (event) {
var data = event.data
// 处理数据
}

与通过send() 发送到服务器的数据一样,event.data 中返回的数据也是字符串。如果你想要得到其他格式的数据,必须手动解析这些数据。

0x03:其他事件
WebSocket 对象还有其他三个事件,在建立生命周期的不同阶段触发。
1.open: 在成功建立连接时触发。
2.error: 在发生错误时触发,连接不能持续。
3.close: 在连接关闭时触发。
WebSocket 对象不支持DOM 2 级事件侦听器,因此必须使用DOM 0 级语法分别定义每个事件处理程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
var socket = new WebSocket('ws://www.example.com/server.php')

socket.onopen = function () {
alert('Connection established.')
}

socket.onerror = function () {
alert('Connection error.')
}

socket.onclose = function () {
alert('Connection closed.')
}

在这三个事件中,只有close 事件的event 对象有额外的信息。这个事件的事件对象有三个额外的属性:wasCleancodereason。其中,wasClean 是一个布尔值,表示连接是否已经明确地关闭;code 是服务器返回的数值状态码;而reason 是一个字符串,包含服务器发回的消息。可以把这些信息显示给用户,也可以记录到日志中以便将来分析。

1
2
3
socket.onclose = function (event) {
console.log('Was clean? ' + event.wasClean + " Code=" + event.code + " Reason=" + event.reason)
}