您的位置:首页 > Web前端 > JavaScript

JavaScript跨域方法总结

2017-03-01 16:58 253 查看
同源安全策略

默认情况下,XHR 对象只能访问与包含它的页面位于同一个域中的资源。这种安全策略可以预防某些恶意行为。但是,实现合理的跨域请求对开发某些浏览器应用程序也是至关重要的。

一、CORS

Cross-Origin Resource Sharing,跨域资源共享

1、原理

CORS是 W3C 的一个工作草案,定义了在必须访问跨源资源时,浏览器与服务器应该如何沟通。CORS 背后的基本思想,就是使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。

比如一个简单的使用 GET 或 POST 发送的请求,它没有自定义的头部,而主体内容是 text/plain。在发送该请求时,需要给它附加一个额外的Origin 头部,其中包含请求页面的源信息(协议、域名和端口) ,以便服务器根据这个头部信息来决定是否给予响应。下面是 Origin 头部的一个示例:

Origin: http://www.nczonline.net

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

Access-Control-Allow-Origin:http://www.nczonline.net

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

2、IE对CORS的实现

微软在 IE8 中引入了 XDR( XDomainRequest )类型。这个对象与 XHR 类似,但能实现安全可靠的跨域通信。XDR 对象的安全机制部分实现了 W3C 的 CORS 规范。以下是 XDR 与 XHR 的一些不同之处。

 cookie 不会随请求发送,也不会随响应返回。

 只能设置请求头部信息中的 Content-Type 字段。

 不能访问响应头部信息。

 只支持 GET 和 POST 请求。

这些变化使 CSRF(Cross-Site Request Forgery,跨站点请求伪造)和 XSS(Cross-Site Scripting,跨站点脚本)的问题得到了缓解。被请求的资源可以根据它认为合适的任意数据(用户代理、来源页面等)来决定是否设置 Access-Control- Allow-Origin 头部。作为请求的一部分, Origin 头部的值表示请求的来源域,以便远程资源明确地识别 XDR 请求。

XDR对象的使用方法与 XHR对象非常相似。 也是创建一个 XDomainRequest 的实例, 调用 open()方法,再调用 send() 方法。但与 XHR 对象的 open() 方法不同,XDR 对象的 open() 方法只接收两个参数:请求的类型和 URL。

所有 XDR 请求都是异步执行的,不能用它来创建同步请求。

3、其他浏览器对CORS的实现

通过 XMLHttpRequest对象实现了对 CORS 的原生支持。在尝试打开不同来源的资源时,无需额外编写代码就可以触发这个行为。 要请求位于另一个域中的资源, 使用标准的 XHR对象并在 open() 方法中传入绝对 URL即可。

xhr.open("get", "http://www.somewhere-else.com/page/", true);


与 IE 中的 XDR 对象不同,通过跨域 XHR 对象可以访问 status 和 statusText 属性,而且还支持同步请求。跨域 XHR 对象也有一些限制,但为了安全这些限制是必需的。以下就是这些限制。

 不能使用 setRequestHeader() 设置自定义头部。

 不能发送和接收 cookie。

 调用 getAllResponseHeaders() 方法总会返回空字符串。

由于无论同源请求还是跨源请求都使用相同的接口,因此对于本地资源,最好使用相对 URL,在访问远程资源时再使用绝对 URL。这样做能消除歧义,避免出现限制访问头部或本地 cookie 信息等问题。

4、Preflighted Reqeusts

CORS 通过一种叫做 Preflighted Requests 的透明服务器验证机制支持开发人员使用自定义的头部、GET 或 POST之外的方法,以及不同类型的主体内容。在使用下列高级选项来发送请求时,就会向服务器发送一个 Preflight 请求。这种请求使用 OPTIONS 方法,发送下列头部。

 Origin :与简单的请求相同。

 Access-Control-Request-Method :请求自身使用的方法。

 Access-Control-Request-Headers :(可选)自定义的头部信息,多个头部以逗号分隔。

以下是一个带有自定义头部 NCZ 的使用 POST 方法发送的请求。

Origin: http://www.nczonline.net Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ


发送这个请求后,服务器可以决定是否允许这种类型的请求。服务器通过在响应中发送如下头部与浏览器进行沟通。

 Access-Control-Allow-Origin :与简单的请求相同。

 Access-Control-Allow-Methods :允许的方法,多个方法以逗号分隔。

 Access-Control-Allow-Headers :允许的头部,多个头部以逗号分隔。

 Access-Control-Max-Age :应该将这个 Preflight 请求缓存多长时间(以秒表示)。

例如:

Access-Control-Allow-Origin: http://www.nczonline.net Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Headers: NCZ
Access-Control-Max-Age: 1728000


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

5、带凭据的请求

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

Access-Control-Allow-Credentials: true


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

6、跨浏览器的CORS

即使浏览器对 CORS 的支持程度并不都一样,但所有浏览器都支持简单的(非 Preflight 和不带凭据的)请求,因此有必要实现一个跨浏览器的方案。检测 XHR 是否支持 CORS 的最简单方式,就是检查是否存在 withCredentials 属性。再结合检测 XDomainRequest 对象是否存在,就可以兼顾所有浏览器了。

function createCORSRequest(method, url){
var xhr = new XMLHttpRequest();
if ("withCredentials" in xhr){
xhr.open(method, url, true);
} else if (typeof XDomainRequest != "undefined"){
vxhr = new XDomainRequest();
xhr.open(method, url);
} else {
xhr = null;
}
return xhr;
}
var request = createCORSRequest("get", "http://www.somewhere-else.com/page/");
if (request){
request.onload = function(){
//对 r
11197
equest.responseText 进行处理
};
request.send();
}


Firefox、Safari 和 Chrome 中的 XMLHttpRequest 对象与 IE 中的 XDomainRequest 对象类似,都提供了够用的接口,因此以上模式还是相当有用的。这两个对象共同的属性/方法如下。

 abort() :用于停止正在进行的请求。

 onerror :用于替代 onreadystatechange 检测错误。

 onload :用于替代 onreadystatechange 检测成功。

 responseText :用于取得响应内容。

 send() :用于发送请求。

以上成员都包含在 createCORSRequest() 函数返回的对象中,在所有浏览器中都能正常使用。

二、图像ping

图像 Ping 是与服务器进行简单、单向的跨域通信的一种方式。

使用 img 标签。请求的数据是通过查询字符串形式发送的,而响应可以是任意内容,但通常是像素图或 204 响应。通过图像 Ping,浏览器得不到任何具体的数据,但通过侦听 load 和 error 事件,它能知道响应是什么时候接收到的。

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 参数。

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

三、JSONP

JSON with padding,填充式 JSON 或参数式 JSON。

JSONP 看起来与 JSON 差不多, 只不过是被包含在函数调用中的 JSON,就像下面这样。

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


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

http://freegeoip.net/json/callback=handleResponse


这个 URL 是在请求一个 JSONP 地理定位服务。 通过查询字符串来指定 JSONP 服务的回调参数是很常见的,就像上面的 URL 所示,这里指定的回调函数的名字叫 handleResponse() 。

JSONP 是通过动态 script元素来使用的,使用时可以为src 属性指定一个跨域 URL。这里的 script 元素与 img 元素类似,都有能力不受限制地从其他域加载资源。因为 JSONP 是有效的 JavaScript 代码,所以在请求完成后,即在 JSONP 响应加载到页面中以后,就会立即执行。来看一个例子。

function handleResponse(response){
alert("You’re at IP address " + response.ip + ", which is in " +
response.city + ", " + response.region_name);
}
var script = document.createElement("script");
script.src = "http://freegeoip.net/json/?callback=handleResponse";
document.body.insertBefore(script, document.body.firstChild);


这个例子通过查询地理定位服务来显示你的 IP 地址和位置信息。

优点

简单易用;

能够直接访问响应文本,支持在浏览器与服务器之间双向通信。

缺点

JSONP 是从其他域中加载代码执行。如果其他域不安全,很可能会在响应中夹带一些恶意代码,而此时除了完全放弃 JSONP 调用之外,没有办法追究。因此在使用不是你自己运维的 Web 服务时,一定得保证它安全可靠。

要确定 JSONP 请求是否失败并不容易。虽然 HTML5 给 script 元素新增了一个 onerror事件处理程序,但目前还没有得到任何浏览器支持。为此,开发人员不得不使用计时器检测指定时间内是否接收到了响应。但就算这样也不能尽如意,毕竟不是每个用户上网的速度和带宽都一样。

四、Comet

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

有两种实现 Comet 的方式:长轮询和流。

1、长轮询

短轮询:浏览器定时向服务器发送请求,看有没有更新的数据。

长轮询:传统轮询(也称为短轮询)的一个翻版,即长轮询把短轮询颠倒了一下。页面发起一个到服务器的请求,然后服务器一直保持连接打开,直到有数据可发送。发送完数据之后,浏览器关闭连接,随即又发起一个到服务器的新请求。这一过程在页面打开期间一直持续不断。

短轮询:



长轮询:



无论是短轮询还是长轮询,浏览器都要在接收数据之前,先发起对服务器的连接。两者最大的区别在于服务器如何发送数据。短轮询是服务器立即发送响应,无论数据是否有效,而长轮询是等待发送响应。轮询的优势是所有浏览器都支持,因为使用 XHR 对象和 setTimeout() 就能实现。而你要做的就是决定什么时候发送请求。

2、http流

流不同于上述两种轮询,因为它在页面的整个生命周期内只使用一个 HTTP 连接。具体来说,就是浏览器向服务器发送一个请求,而服务器保持连接打开,然后周期性地向浏览器发送数据。

下面这段 PHP 脚本就是采用流实现的服务器中常见的形式。

<?php
$i = 0;
while(true){
//输出一些数据,然后立即刷新输出缓存
echo "Number is $i";
flush();

//等几秒钟
sleep(10);
$i++;
}
php>


所有服务器端语言都支持打印到输出缓存然后刷新(将输出缓存中的内容一次性全部发送到客户端)的功能。而这正是实现 HTTP 流的关键所在。

在 Firefox、Safari、Opera 和 Chrome 中,通过侦听 readystatechange 事件及检测 readyState

的值是否为 3,就可以利用 XHR 对象实现 HTTP 流。在上述这些浏览器中,随着不断从服务器接收数据, readyState 的值会周期性地变为 3。当 readyState 值变为 3 时, responseText 属性中就会保存接收到的所有数据。此时,就需要比较此前接收到的数据,决定从什么位置开始取得最新的数据。使用 XHR 对象实现 HTTP 流的典型代码如下所示。

function createStreamingClient(url, progress, finished){
var xhr = new XMLHttpRequest(),
received = 0;
xhr.open("get", url, true);
xhr.onreadystatechange = function(){
var result;
if (xhr.readyState == 3){
//只取得最新数据并调整计数器
result = xhr.responseText.substring(received);
received += result.length;
//调用 progress 回调函数
progress(result);
} else if (xhr.readyState == 4){
finished(xhr.responseText);
}
};
xhr.send(null);
return xhr;
}
var client = createStreamingClient("streaming.php", function(data){
alert("Received: " + data);
}, function(data){
alert("Done!");
});


这个 createStreamingClient() 函数接收三个参数:要连接的 URL、在接收到数据时调用的函数以及关闭连接时调用的函数。有时候,当连接关闭时,很可能还需要重新建立,所以关注连接什么时候关闭还是有必要的。

只要 readystatechange 事件发生,而且readyState 值为 3,就对 responseText 进行分割以取得最新数据。 这里的 received 变量用于记录已经处理了多少个字符, 每次 readyState 值为 3 时都递增。然后,通过 progress 回调函数来处理传入的新数据。而当 readyState 值为 4 时,则执行finished 回调函数,传入响应返回的全部内容。

为简化Comet的两个新接口:SSE、WebSockets

3、服务器发送事件【SSE】

SSE(Server-Sent Events,服务器发送事件)是围绕只读 Comet 交互推出的 API 或者模式。

SSE API用于创建到服务器的单向连接,服务器通过这个连接可以发送任意数量的数据。服务器响应的 MIME类型必须是 text/event-stream ,而且是浏览器中的 JavaScript API 能解析格式输出。SSE 支持短轮询、长轮询和 HTTP 流,而且能在断开连接时自动确定何时重新连接。

SSE API

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

var source = new EventSource("myevents.php");


注意,传入的 URL 必须与创建对象的页面同源(相同的 URL 模式、域及端口) 。 EventSource 的实例有一个 readyState 属性,值为 0 表示正连接到服务器,值为 1 表示打开了连接,值为 2 表示关闭了连接。

另外,还有以下三个事件。

 open :在建立连接时触发。

 message :在从服务器接收到新事件时触发。

 error :在无法建立连接时触发。

就一般的用法而言, onmessage 事件处理程序也没有什么特别的。

source.onmessage = function(event){
var data = event.data;
//处理数据
};


服务器发回的数据以字符串形式保存在 event.data 中。

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

source.close();


事件流

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

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: 行前面或后面皆可:

data: foo
id: 1


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

4、Web Sockets

Web Sockets的目标是在一个单独的持久连接上提供全双工、双向通信。

在 JavaScript 中创建了 Web Socket 之后,会有一个 HTTP 请求发送到浏览器以发起连接。在取得服务器响应后,建立的连接会使用 HTTP 升级从 HTTP 协议交换为 WebSocket 协议。也就是说,使用标准的 HTTP 服务器无法实现 Web Sockets,只有支持这种协议的专门服务器才能正常工作。

由于 Web Sockets使用了自定义的协议, 所以 URL 模式也略有不同。 未加密的连接不再是 http:// ,而是 ws:// ;加密的连接也不是 https:// ,而是 wss:// 。在使用 Web Socket URL 时,必须带着这个模式,因为将来还有可能支持其他模式。

使用自定义协议而非 HTTP 协议的好处

能够在客户端和服务器之间发送非常少量的数据,而不必担心 HTTP 那样字节级的开销。由于传递的数据包很小,因此 Web Sockets非常适合移动应用。毕竟对移动应用而言,带宽和网络延迟都是关键问题。

缺点

制定协议的时间比制定JavaScript API 的时间还要长。Web Sockets曾几度搁浅,就因为不断有人发现这个新协议存在一致性和安全性的问题。

4.1. Web Sockets API

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

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


注意,必须给 WebSocket 构造函数传入绝对 URL。同源策略对 Web Sockets 不适用,因此可以通过它打开到任何站点的连接。至于是否会与某个域中的页面通信,则完全取决于服务器。 (通过握手信息就可以知道请求来自何方。 )

实例化了 WebSocket 对象后,浏览器就会马上尝试创建连接。与 XHR 类似, WebSocket 也有一个表示当前状态的 readyState 属性。不过,这个属性的值与 XHR 并不相同,而是如下所示。

 WebSocket.OPENING (0):正在建立连接。

 WebSocket.OPEN (1):已经建立连接。

 WebSocket.CLOSING (2):正在关闭连接。

 WebSocket.CLOSE (3):已经关闭连接。

WebSocket 没有 readystatechange 事件; 不过, 它有其他事件, 对应着不同的状态。 readyState的值永远从 0 开始。

要关闭 Web Socket 连接,可以在任何时候调用 close() 方法。

socket.close();


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

4.2. 发送和接收数据

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

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


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

var message = {
time: new Date(),
text: "Hello world!",
clientId: "asdfp8734rew"
};
socket.send(JSON.stringify(message));


接下来,服务器要读取其中的数据,就要解析接收到的 JSON 字符串。

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

socket.onmessage = function(event){
var data = event.data;
//处理数据
};


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

4.3. 其他事件

WebSocket 对象还有其他三个事件,在连接生命周期的不同阶段触发。

 open :在成功建立连接时触发。

 error :在发生错误时触发,连接不能持续。

 close :在连接关闭时触发。

WebSocket 对象不支持 DOM 2 级事件侦听器,因此必须使用 DOM 0 级语法分别定义每个事件处理程序。

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

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


5、SSE与Web Sockets

面对某个具体的用例,在考虑是使用 SSE 还是使用 Web Sockets 时,可以考虑如下几个因素。

① 你是否有自由度建立和维护 Web Sockets服务器?因为 Web Socket 协议不同于 HTTP,所以现有服务器

不能用于 Web Socket 通信。SSE 倒是通过常规 HTTP 通信,因此现有服务器就可以满足需求。

② 到底需不需要双向通信。如果用例只需读取服务器数据(如比赛成绩) ,那么 SSE 比较容易实现。如果用例必须双向通信(如聊天室) ,那么 Web Sockets 显然更好。别忘了,在不能选择 Web Sockets 的情况下,组合 XHR 和 SSE 也是能实现双向通信的。

5. document.domain

将页面的document.domain设置为相同的值,页面间可以互相访问对方的JavaScript对象。

注意:

不能将值设置为URL中不包含的域;

松散的域名不能再设置为紧绷的域名。

本文摘录自《JavaScript高级程序设计(第3版)》
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息