一、跨源资源共享
同源策略是对 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高级函数