45-JavaScript高级程序设计-跨域

一、跨源资源共享

同源策略是对 XHR 的一个主要约束,它为通信设置了“相同的域、相同的端口、相同的协议”这一限制。

1. CORS

通过 XHR 实现 Ajax 通信的一个主要限制,来源于跨域安全策略。
默认情况下,XHR 对象只能访问与包含它的页面位于同一个域中的资源。这种安全策略可以预防某些恶意行为。

CORS(Cross-Origin Resource Sharing,跨源资源共享),定义了在必须访问跨源资源时,浏览器与服务器应该如何沟通。
CORS 背后的基本思想:使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。

示例:
发送请求时,附加一个额外的 Origin 头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给予响应。
Origin: http://www.nczonline.net
如果服务器认为这个请求可以接受,就在 Access-Control-Allow-Origin 头部中回发相同的源信息(如果是公共资源,可以回发"*")。如果没有这个头部,或者有这个头部但源信息不匹配,浏览器就会驳回请求。
Access-Control-Allow-Origin: http://www.nczonline.net

2. IE对CORS的实现

微软在 IE8 中引入了 XDR(XDomainRequest) 类型。这个对象与 XHR 类似,但能实现安全可靠的跨域通信。
XDR 与 XHR 的一些不同之处:
1、cookie 不会随请求发送,也不会随响应返回。
2、只能设置请求头部信息中的 Content-Type 字段。
3、不能访问响应头部信息。
4、只支持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 请求都是异步执行的,不能用它来创建同步请求。

请求返回之后,会触发 load 事件,响应的数据也会保存在 responseText 属性中。
在接收到响应后,只能访问响应的原始文本;没有办法确定响应的状态代码。
只要响应有效就会触发 load 事件,如果失败(包括响应中缺少 Access-Control-Allow-Origin 头部)就会触发 error 事件。error 事件只能确定请求未成功,没有其他信息可用。

在请求返回前调用 abort() 方法可以终止请求:xdr.abort(); //终止请求

与 XHR 一样,XDR 对象也支持 timeout 属性以及 ontimeout 事件处理程序。
为支持 POST 请求,XDR 对象提供了 contentType 属性,用来表示发送数据的格式。

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

Firefox 3.5+、Safari 4+、Chrome、iOS 版 Safari 和 Android 平台中的 WebKit 都通过 XMLHttpRequest 对象实现了对 CORS 的原生支持。
要请求位于另一个域中的资源,使用标准的 XHR 对象并在 open()方法中传入绝对 URL 即可。
xhr.open("get", "http://www.somewhere-else.com/page/", true);

与 IE 中的 XDR 对象不同,通过跨域 XHR 对象可以访问 status 和 statusText 属性,而且还支持同步请求。
跨域 XHR 对象也有一些限制,但为了安全这些限制是必需的。
1、不能使用 setRequestHeader() 设置自定义头部。
2、不能发送和接收 cookie。
3、调用 getAllResponseHeaders() 方法总会返回空字符串。

由于无论同源请求还是跨源请求都使用相同的接口,因此对于本地资源,最好使用相对 URL,在访问远程资源时再使用绝对 URL。

4. Preflighted Reqeusts

CORS 通过一种叫做 Preflighted Requests 的透明服务器验证机制支持开发人员使用自定义的头部、GET 或 POST 之外的方法,以及不同类型的主体内容。

在使用下列高级选项来发送请求时,就会向服务器发送一个 Preflight 请求。这种请求使用 OPTIONS 方法,发送下列头部。
Origin:与简单的请求相同,请求页面的源信息。
Access-Control-Request-Method:请求自身使用的方法。
Access-Control-Request-Headers:(可选)自定义的头部信息,多个头部以逗号分隔。

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

支持 Preflight 请求的浏览器:包括 Firefox 3.5+、Safari 4+ 和 Chrome。IE 10 及更早版本都不支持。

5. 带凭据的请求

默认情况下,跨源请求不提供凭据 (cookie、HTTP 认证及客户端 SSL 证明等)。
通过将 withCredentials 属性设置为 true,可以指定某个请求应该发送凭据。
Access-Control-Allow-Credentials: true

如果发送的是带凭据的请求,但服务器的响应中没有包含这个头部,那么浏览器就不会把响应交给 JavaScript。
–> responseText 中将是空字符串,status 的值为 0,而且会调用 onerror() 事件处理程序。

支持 withCredentials 属性的浏览器:Firefox 3.5+、Safari 4+ 和 Chrome。IE 10 及更早版本都不支持。

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"){
        xhr = 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(){
        //对 request.responseText 进行处理
    };
    request.send();
}

二、其他跨域技术

不使用CORS,利用 DOM 中能够执行跨域请求的功能,在不依赖 XHR 对象的情况下也能发送某种请求。

1. 图像Ping

第一种跨域请求技术是使用<img>标签。一个网页可以从任何网页中加载图像,不用担心跨域不跨域。

可以动态地创建图像,使用它们的 onload 和 onerror 事件处理程序来确定是否接收到了响应。
动态创建图像经常用于图像 Ping。
图像 Ping 是与服务器进行简单、单向的跨域通信的一种方式。请求的数据是通过查询字符串形式发送的,而响应可以是任意内容,通常是像素图或 204 响应。
通过图像 Ping,浏览器得不到任何具体的数据,但通过侦听 load 和 error 事件,能知道响应是什么时候接收到的。

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

2. JSONP

JSONP 是 JSON with padding (填充式 JSON 或参数式 JSON) 的简写,JSONP 看起来与 JSON 差不多,只不过是被包含在函数调用中的 JSON。
callback({ "name": "Nicholas" });

JSONP 由两部分组成:回调函数和数据。
回调函数是当响应到来时应该在页面中调用的函数。回调函数的名字一般是在请求中指定的。数据就是传入回调函数中的 JSON 数据。
一个典型的JSONP请求:http://freegeoip.net/json/?callback=handleResponse

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

与图像 Ping 相比,JSONP 的优点:能够直接访问响应文本,支持在浏览器与服务器之间双向通信。
两点不足:
JSONP 是从其他域中加载代码执行。如果其他域不安全,很可能会在响应中夹带一些恶意代码,此时除了完全放弃 JSONP 调用之外,没有办法追究。所以一定得保证它安全可靠。
要确定 JSONP 请求是否失败并不容易。

3. Comet

Comet:指的是一种更高级的 Ajax 技术(经常也有人称为“服务器推送”)。Ajax 是一种从页面向服务器请求数据的技术,Comet 是一种服务器向页面推送数据的技术。

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

1.长轮询

长轮询是传统轮询(也称为短轮询)的一个翻版,即浏览器定时向服务器发送请求,看有没有更新的数据。

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

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

2. HTTP流

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

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

在 Firefox、Safari、Opera 和 Chrome 中,通过侦听 readystatechange 事件及检测 readyState 的值是否为 3,就可以利用 XHR 对象实现 HTTP 流。

4. 服务器发送事件

SSE(Server-Sent Events,服务器发送事件)是围绕只读 Comet 交互推出的 API 或者模式。
SSE API 用于创建到服务器的单向连接,服务器通过这个连接可以发送任意数量的数据。服务器响应的 MIME 类型必须是 text/event-stream,而且是浏览器中的 JavaScript API 能解析格式输出。
SSE 支持短轮询、长轮询和 HTTP 流,而且能在断开连接时自动确定何时重新连接。
支持 SSE 的浏览器:Firefox 6+、Safari 5+、Opera 11+、Chrome 和 iOS 4+版 Safari。

SSE API

要预订新的事件流,首先要创建一个新的 EventSource 对象,并传进一个入口点:
var source = new EventSource("myevents.php");
注意:传入的 URL 必须与创建对象的页面同源 (相同的 URL 模式、域及端口)。

EventSource 的实例中的属性和方法:
一个属性:
readyState:值为 0 表示正连接到服务器,值为 1 表示打开了连接,值为 2 表示关闭了连接。
三个事件:
open:在建立连接时触发。
message:在从服务器接收到新事件时触发。
error:在无法建立连接时触发。

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

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

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

所谓的服务器事件会通过一个持久的 HTTP 响应发送,这个响应的 MIME 类型为 text/event-stream。响应的格式是纯文本,最简单的情况是每个数据项都带有前缀 data: 。
只有在包含 data: 的数据行后面有空行时,才会触发 message 事件,因此在服务器上生成事件流时不能忘了多添加这一行。
通过 id:前缀可以给特定的事件指定一个关联的 ID,这个 ID 行位于 data:行前面或后面皆可。
设置了 ID 后,EventSource 对象会跟踪上一次触发的事件。如果连接断开,会向服务器发送一个包含名为 Last-Event-ID 的特殊 HTTP 头部的请求,以便服务器知道下一次该触发哪个事件。在多次连接的事件流中,这种机制可以确保浏览器以正确的顺序收到连接的数据段。

5. Web Sockets ⭐️

Web Sockets 的目标是在一个单独的持久连接上提供全双工、双向通信。
在 JavaScript 中创建了 Web Socket 之后,会有一个 HTTP 请求发送到浏览器以发起连接。在取得服务器响应后,建立的连接会使用 HTTP 升级从 HTTP 协议交换为 Web Socket 协议。
URL 模式:未加密的连接不是 http://,而是 ws://;加密的连接不是 https://,而是 wss://。

使用自定义协议而非 HTTP 协议的好处:能够在客户端和服务器之间发送非常少量的数据,而不必担心 HTTP 那样字节级的开销。由于传递的数据包很小,因此 Web Sockets 非常适合移动应用。
使用自定义协议的缺点:制定协议的时间比制定 JavaScript API 的时间还要长。
支持 Web Sockets 的浏览器:Firefox 6+、Safari 5+、Chrome 和 iOS 4+版 Safari。

Web Sockets API

创建 Web Socket,先实例一个 WebSocket 对象并传入要连接的 URL:
var socket = new WebSocket("ws://www.example.com/server.php");
注意:必须给 WebSocket 构造函数传入绝对 URL。同源策略对 Web Sockets 不适用,因此可以通过它打开到任何站点的连接。

WebSocket 也有一个表示当前状态的 readyState 属性:
WebSocket.OPENING (0):正在建立连接。
WebSocket.OPEN (1):已经建立连接。
WebSocket.CLOSING (2):正在关闭连接。
WebSocket.CLOSE (3):已经关闭连接。

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

发送和接收数据

要向服务器发送数据,使用 send() 方法并传入任意字符串。
socket.send("Hello world!");
Web Sockets只能通过连接发送纯文本数据,对于复杂的数据结构,在通过连接发送之前,必须进行序列化。

当服务器向客户端发来消息时,WebSocket 对象就会触发 message 事件。返回的数据保存在 event.data 属性中。

其他事件

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 是一个字符串,包含服务器发回的消息。

6. SSE与Web Sockets

是使用 SSE 还是使用 Web Sockets,可以考虑如下几个因素:

  • 是否有自由度建立和维护 Web Sockets服务器?
    因为 Web Socket 协议不同于 HTTP,所以现有服务器不能用于 Web Socket 通信。SSE 倒是通过常规 HTTP 通信,因此现有服务器就可以满足需求。
  • 到底需不需要双向通信?
    如果用例只需读取服务器数据(如比赛成绩),那么 SSE 比较容易实现。如果用例必须双向通信(如聊天室),那么 Web Sockets 显然更好。
    在不能选择 Web Sockets 的情况下,组合 XHR 和 SSE 也是能实现双向通信的。

三、安全

可以通过 XHR 访问的任何 URL 也可以通过浏览器或服务器来访问。
对于未被授权系统有权访问某个资源的情况,称之为 CSRF(Cross-Site Request Forgery,跨站点请求伪造)。

为确保通过 XHR 访问的 URL 安全,通行的做法就是验证发送请求者是否有权限访问相应的资源。
方式1:要求以 SSL 连接来访问可以通过 XHR 请求的资源。
方式2:要求每一次请求都要附带经过相应算法计算得到的验证码。

对防范 CSRF 攻击不起作用的措施:
要求发送 POST 而不是 GET 请求——很容易改变。
检查来源 URL 以确定是否可信——来源记录很容易伪造。
基于 cookie 信息进行验证——同样很容易伪造。

ps:了解有关 Ajax 的更多信息,参考《Ajax 高级程序设计(第 2 版)》。


上一篇:44-JavaScript高级程序设计-XMLHttpRequest对象
下一篇:46-JavaScript高级程序设计-高级技巧1高级函数

全书整理版:《Javascript高级程序设计》第3版(总结版)


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值