本篇内容
  • XMLHttpRequest对象
  • XMLHttpRequest2级
  • 进度事件
  • 跨源资源共享
  • 其他跨域技术
  • 安全

2005年,Jesse James Garrett发表了一篇在线文章,题为”Ajax: A new Approach to Web Applications”。他在这篇文章里介绍了一种技术,用他的话说,就叫Ajax,是对Asynchronous JavaScript+XML的简写。这一技术能够向服务器请求额外的数据而无须卸载页面,会带来更好的用户体验。Garrett还解释了怎样使用这一技术改变自从Web诞生以来就一直沿用的“单击,等待”的交互模式。

Ajax技术的核心是XMLHttpRequest对象(简称XHR),这是由微软首先引入的一个特性,其他浏览器提供商后来都提供了相同的实现。在XHR出现之前,Ajax式的通信必须借助一些hack手段来实现,大多数是使用隐藏的框架或内嵌框架。 XHR为向服务器发送请求和解析服务器响应提供了流畅的接口。能够以异步方式从服务器取得更多信息,意味着用户单击后,可以不必刷新页面也能取得新数据。 也就是说,可以使用XHR对象取得新数据,然后再通过DOM将新数据插入到页面中。另外,虽然名字中包含XML的成分,但Ajax通信与数据格式无关;这种技术就是无须刷新页面即可从服务帮取得数据,但不一定是XML数据。

实际上,Garrett提到的这种技术已经存在很长时间了。在Garrett撰写那篇文章之前,人们通常将这种技术叫做远程脚本(remote scripting),而且早在1998年就有人采用不同的手段实现了这种浏览器与服务器的通信。再往前推,JavaScript需要通过Java applet或Flash电影等中间层向服务端发送请求。而XHR则将浏览器原生的通信能力提供给了开发人员,简化了实现同样操作的任务。
在重命名为Ajax之后,大约是2005年底2006年初,这种浏览器与服务器的通信技术可谓红极一时。人们对JavaScript和Web的全新认识,催生了很多使用原有特性的新技术和新模式。就目前来说,熟练使用XHR对象已经成为所有Web开发人员必须掌握的一种技能。

1 XMLHttpRequest对象

IE5是第一款引入XHR对象的浏览器。在IE5中,XHR对象是通过MSXML库中的一个ActiveX对象实现的。因此,在IE中可能会遇到三种不同版本的XHR对象,即MSXML2.XMLHttp, MSXML2.XMLHttp3.0和MSXML2.XMLHttp6.0。
如下使用MSXML库中的XHR对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 适用于IE7之前的版本
function createXHR () {
if (typeof arguments.callee.activeXString !== 'string') {
var versions = ['MSXML2.XMLHttp6.0', 'MSXML2.XMLHttp3.0', 'MSXML2.XMLHttp'];
var i, len;

for (i = 0, len = versions.length; i < len; i++) {
try {
new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
break;
} catch (e) {

}
}
}

return new ActiveXObject(arguments.callee.activeXString);
}

这个函数会尽力根据IE中可用的MSXML库的情况创建最新版本的XHR对象。IE7+、Firefox、Opera、Chrome和Safari都支持原生的XHR对象,在这些浏览器中创建XHR对象要像下面这样使用XMLHttpRequest构造函数。

1
var xhr = new XMLHttpRequest();

综合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function createXHR () {
if (typeof XMLHttpRequest !== 'undefined') {
return new XMLHttpRequest();
}

if (!ActiveXObject) throw new Error('Error!No XHR object available.');

if (typeof arguments.callee.activeXString !== 'string') {
var versions = ['MSXML2.XMLHttp6.0', 'MSXML2.XMLHttp3.0', 'MSXML2.XMLHttp'];
var i, len;

for (i = 0, len = versions.length; i < len; i++) {
try {
new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
break;
} catch (e) {

}
}
}

return new ActiveXObject(arguments.callee.activeXString);
}

1.1 XHR的用法

在使用XHR对象时,要调用的第一个方法是open(),它接受3个参数:要发送的请求的类型(”get”、”post”等)、请求的URL和表示是否异步发送请求的布尔值。下面就是调用这个方法的例子。

1
xhr.open('get', 'test.php', false);

这行代码会启动一个针对test.php的GET请求。有关这行代码,需要说明两点:

  • 一是URL相对于执行代码的当前页面(当然也可以使用绝对路径);
  • 二是调用open()方法并不会真正发送请求,而只是启动一个请求以备发送。

只能向同一个域中使用相同端口和协议的URL发送请求。如果URL与启动请求的页面有任何差别,都会引发安全错误。

要发送特定的请求,必须像下面这样调用send()方法:

1
2
xhr.open('get', 'test.php', false);
xhr.send(null);

这里的send()方法接收-个参数,即要作为请求主体发送的数据。如果不需要通过请求主体发送数据,则必须传入null,因为这个参数对有些浏览器来说是必需的。调用send()之后,请求就会被分派到服务器。
由于这次请求是同步的,JavaScript代码会等到服务器响应之后再继续执行。在收到响应后,响应的数据会自动填充XHR对象的属性,相关的属性简介如下。

  • responseText:作为响应主体被返回的文本。
  • responseXML:如果响应的内容类型是”text/xml”或“application/xml”,这个属性中将保存包含着响应数据的XML DOM文档。
  • status:响应的HTTP状态。
  • statusText:HTTP状态的说明。

在接收到响应后,第-步是检查status属性,以确定响应已经成功返回。一般来说,可以将HTTP状态代码为200作为成功的标志。此时,responseText属性的内容已经就绪,而且在内容类型正确的情况下responseXML也应该能够访问了。此外,状态代码为304表示请求的资源并没有被修改,可以直接使用浏览器中缓存的版本;当然,也意味着响应是有效的。为确保接收到适当的响应,应该像下面这样检查上述这两种状态代码:

1
2
3
4
5
6
7
8
9
xhr.open('get', 'test.php', false);
xhr.send(null);

if (xhr.status >= 200 && xhr.status < 300 ||
xhr.status == 304) {
alert(xhr.responseText);
} else {
alert('Request was unsuccessful: ' + xhr.status);
}

根据返回的状态代码,这个例子可能会显示自服务器返回的内容,也可能会显示一条错误消息。我们建议读者要通过检测status来决定下一步的操作,不要依赖statusText,因为后者在跨浏览器使用时不太可靠。另外,无论内容类型是什么,响应主体的内容都会保存到responseText属性中;而对于非XML数据而言,responseXML属性的值将为null。

有的浏览器会错误地报告加状态代码。IE中XHR的ActiveX版本会将204设置为1223,而IE中原生的XHR则会将204规范化为200。Opera会在取得204时报告status的值为0。

像前面这样发送同步请求当然没有问题,但多数情况下,我们还是要发送异步请求,才能让JavaScript继续执行而不必等待响应。此时.可以检测XHR对象的readyState属性,该属性表示请求/响应过程的当前活动阶段。这个属性可取的值如下。

  • 0:未初始化。尚未调用open()方法。
  • 1:启动。已经调用open()方法,但尚未调用send()方法。
  • 2:发送。已经调用send()方法,但尚未接收到响应。
  • 3:接收。已经接收到部分响应数据。
  • 4:完成。已经接收到全部响应数据,而且已经可以在客户端使用了。

只要readyState属性的值由一个值变成另一个值,都会触发一次readystatechange事件。可以利用这个事件来检测每次状态变化后readyState的值。通常,我们只对readyState值为4的阶段感兴趣,因为这时所有数据都已经就绪。不过.必须在调用open()之前指定onreadystatechange事件处理程序才能确保跨浏览器兼容性。下面来看一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var xhr = createXHR();
xhr.onreadystatechange = function () {
if (xhr.readState === 4) {
if (xhr.status >= 200 && xhr.status < 300 ||
xhr.status == 304) {
alert(xhr.responseText);
} else {
alert('Request was unsuccessful: ' + xhr.status);
}
}
};

xhr.open('get', 'test.php', false);
xhr.send(null);

以上代码利用DOM0级方法为XHR对象添加了事件处理程序,原因是并非所有浏览器都支持DOM2级方法。与其他事件处理程序不同,这里没有向onreadystatechange事件处理程序中传递event对象;必须通过XHR对象本身来确定下一步该怎么做。

这个例子在onreadystatechange事件处理程序中使用了xhr对象,没有使用this对象,原因是onreadystatechange时间处理程序的作用域问题。如果使用this对象,在有的浏览器中会导致函数执行失败,或者导致错误发生。因此,使用时机的XHR对象实例变量是较为可靠的一种方式。

另外,在接收到响应之前还可以调用abort()方法来取消异步请求,如下所示:

1
xhr.abort();

调用这个方法后,XHR对象会停止触发事件.而且也不再允许访问任何与响应有关的对象属性。在终止请求之后.还应该对XHR对象进行解引用操作。由于内存原因,不建议重用XHR对象。

1.2 HTTP 头部信息

每个HTTP请求和响应都会带有相应的头部信息,其中有的对开发人员有用,有的也没有什么用。
XHR对象也提供了操作这两种头部(即请求头部和响应头部)信息的方法。
默认情况下,在发送XHR请求的同时,还会发送下列头部信息。

  • Accept:浏览器能够处理的内容类型。
  • Accept-Charset:浏览器能够显示的字符集。
  • Accept-Encoding:浏览器能够处理的压缩编码。
  • Accept-Language:浏览器当前设置的语言。
  • Connection:浏览器与服务器之间连接的类型。
  • Cookie:当前页面设置的任何Cookie。
  • Host:发出请求的也没所在的域。
  • Referer:发出请求的页面的URI。注意,Http规范将这个头部字段拼写错了,而为保证与规范一致,也只能将错就错(正确拼写是referrer)。
  • User-Agent:浏览器的用户代理字符串。

虽然不同浏览器实际发送的头部信息会有所不同,但以上列出的基本上是所有浏览器都会发送的。
使用setRequestHeader()方法可以设置自定义的请求头部信息。这个方法接受两个参数:头部字段的名称和头部字段的值。要成功发邀请求头部信息,必须在调用open()方法之后且调用send()方法之前调用setRequestHeader(),如下面的例子所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var xhr = createXHR();
xhr.onreadystatechange = function () {
if (xhr.readState === 4) {
if (xhr.status >= 200 && xhr.status < 300 ||
xhr.status == 304) {
alert(xhr.responseText);
} else {
alert('Request was unsuccessful: ' + xhr.status);
}
}
};

xhr.open('get', 'test.php', false);
xhr.setRequestHeader('MyHeader', 'MyValue');
xhr.send(null);

服务端在接收到这种自定义的头部信息之后,可以执行相应的后续操作。我们建议读者使用自定义的头部字段名称,不要使用浏览器正常发送的字段名称,否则有可能会影响服务器的响应。有的浏览器允许开发人员重写默认的头部宿息,但有的浏览器则不允许这样做。
调用XHR对象的getResponseHeader()方法并传入头部字段名称,可以取得相应的响应头部信息。而调用getAllResponseHeaders()方法则可以取得一个包含所有头部信息的长字符串。来看下面的例子。

1
2
var myHeader = xhr.getResponseHeader('MyHeader');
var allHeaders = xhr.getAllResponseHeaders();

在服务器端,也可以利用头部信息向浏览器发送额外的、结构化的数据。在没有自定义信息的情况下,getAllResponseHeaders()方法通常会返回如下所示的多行文本内容:

1
2
3
4
5
6
Date: Sun, 14 Nov 2010 18:04:03 GMT 
Server: Apache/1.3.29 (Unix)
Vary: Accept
X-Powered-By: PHP/4.3.8
Connection: close
Content-Type: text/html; charset=iso-8859-1

这种格式化的输出可以方使我们检查响应中所有头部字段的名称,而不必一个一个地检查某个字段是否存在。

1.3 GET 请求

GET是最常见的请求类型,最常用于向服务器查询某些信息。必要时,可以将查询字串参数追加到URL的末尾,以便将信息发送给服务器。对XHR而言,位于传入open()方法的URL末尾的查询字符串必须经过正确的编码才行。
使用GET请求经常会发生的一个错误,就是查询字符串的格式有问题。查询字符串中每个参数的名称和值都必须使用encodeURIComponent()进行编码,然后才能放到URL的末尾;而且所有名-值对儿都必须由和号(&)分隔,如下面的例子所示。

1
xhr.open('get', 'test.php?name=micheal&time=2018', true);

下面这个函数可以辅助向现有URL的末尾添加查询字符串参数:

1
2
3
4
5
function addURLParam(url, name, value) {
url += url.indexOf('?') === -1 ? '?' : '&';
url += encodeURIComponent(name) + '=' + encodeURIComponent(value);
return url;
}

使用

1
2
3
4
5
var url = 'test.php';
url = addURLParam(url, 'time', '2018');
url = addURLParam(url, 'name', 'micheal');

xhr.open('get', url, false);

1.4 POST请求

使用频率仅次于GET的是POST请求,通常用于向服务器发送应该被保存的数据。POST请求应该把数据作为请求的主体提交,而GET请求传统上不是这样。POST请求的主体可以包含非常多的数据,而且格式不限。在open()方法第一个参数的位置传入”post”,就可以初始化一个POST请求,如下面的例子所示。

1
xhr.open('post', 'test.php', true);

发送POST请求的第二步就是向send()方法中传入某些数据。由于XHR最初的设计主要是为了处理XML,因此可以在此传入XML DOM文档。传入的文档经序列化之后将作为请求主体被提交到服务器。当然,也可以在此传入任何想发送到服务器的字符串。
默认情况下,服务器对POST请求和提交Web表单的请求并不会一视同仁。因此.服务指端必须有程序来读取发送过来的原始数据,并从中解析出有用的部分。不过,我们可以使用XHR来模仿表单提交:首先将Content-Type头部信息设置为application/x-www-form-urlencoded,也就是表单提交时的内容类型,其次是以适当的格式创建一个字符串。POST数据的格式与查询字符串格式相同。如果需要将页面中表单的数据进行序列化,然后再通过XHR发送到服务器,那么就可以使用之前介绍的serialize()函数来创建这个字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function submitData () {
var xhr = createXHR();
xhr.onreadystatechange = function () {
if (xhr.readState === 4) {
if (xhr.status >= 200 && xhr.status < 300 ||
xhr.status == 304) {
alert(xhr.responseText);
} else {
alert('Request was unsuccessful: ' + xhr.status);
}
}
};
}

xhr.open('post', 'test.php', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
var form = document.getElementById('user-info');
xhr.send(serialize(form));

与GET请求相比,POST请求消耗的协会更多一些。从性能角度来看,以发送相同的数据计,GET请求的速度最多可达到POST请求的两倍。

2 XMLHttpRequest2级

鉴于XHR已经得到广泛接受,成为了事实标准,W3C也着手制定相应的标准以规范其行为。XMLHttpRequest1级只是把已有的XHR对象的实现细节描述了出来。而XMLHttpRequest2级则进一步发展了XHR。并非所有浏览器都完整地实现了XMLHttpRequest2级规范,但所有浏览器都实现了它规定的部分内容。

2.1 FormData

现代Web应用中频繁使用的一项功能就是表单数据的序列化,XMLHttpRequest2级为此定义了FormData类型。FormData为序列化表单以及创建与表单格式相同的数据(用于通过XHR传输)提供了便利。下面的代码创建了一个FormData对象,并向其中添加了一些数据。

1
2
var data = new FormData();
data.append('name', 'micheal');

这个append()方法接收两个参数:键和值,分别对应表单字段的名字和字段中包含的值。可以像这样添加任意多个键值对儿。而通过向FormData构造函数中传入表单元素,也可以用表单元素的数据 预先向其中填入键值对儿:

1
var data = new FormData(document.forms[0]);

创建了FormData的实例后,可以将它直接传给XHR的send()方法,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var xhr = createXHR();
xhr.onreadystatechange = function () {
if (xhr.readState === 4) {
if (xhr.status >= 200 && xhr.status < 300 ||
xhr.status == 304) {
alert(xhr.responseText);
} else {
alert('Request was unsuccessful: ' + xhr.status);
}
}
};

xhr.open('post', 'test.php', true);
var form = document.getElementById('user-info');
xhr.send(new FormData(form));

使用FormData的方便之处体现在不必明确地在XHR对象上设置请求头部。XHR对象能够识别传人的数据类型是FormData的实例,并配置适当的头部信息。

支持FormData的浏览器有Firefox4+、Safari5+、Chrome和Android3+版WebKit。

2.2 超时设定

IE8为XHR对象添加了一个timeout属性,表示请求在等待响应多少毫秒之后就终止。在给timeout设置一个数值后,如果在规定的时间内浏览器还没有接收到响应,那么就会触发timeout事件,进而会调用ontimeout事件处理程序。这项功能后来也被收入了XMLHttpRequest2级规范中。来看下面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var xhr = createXHR();
xhr.onreadystatechange = function () {
if (xhr.readState === 4) {
if (xhr.status >= 200 && xhr.status < 300 ||
xhr.status == 304) {
alert(xhr.responseText);
} else {
alert('Request was unsuccessful: ' + xhr.status);
}
}
};

xhr.open('post', 'test.php', true);
xhr.timeout = 1000;
xhr.ontimeout = function () {
alert('Request did not return in a second.');
};
var form = document.getElementById('user-info');
xhr.send(new FormData(form));

这个例子示范了如何使用timeout属性。将这个属性设置为1000毫秒,意味着如果请求在1秒钟内还没有返回,就会自动终止。请求终止时,会调用ontimeout事件处理程序。但此时readyState可能已经改变为4了,这意味着会调用onreadystatechange事件处理程序。可是,如果在超时终止请求之后再访问status属性,就会导致错误。为避免浏览器报告错误,可以将检查status属性的话句封装在一个try-catch语句当中。

IE兼容:IE8+

2.3 overrideMimeType()方法

Firefox最早引入了overrideMimeType()方法,用于重写XHR响应的MIME类型。这个方法后来也被纳入了XMLHttpRequest2级规范。因为返回响应的MIME类型决定了XHR对象如何处理它,所以提供一种方法能够重写服务帮返回的MIME类型是很有用的。
比如,服务器返回的MIME类型是text/plain,但数据中实际包含的是XML。根据MIME类型,即使数据是XML,responseXML属性中仍然是null。通过调用verrideMimeType()方法,可以保证把响应当作XML而非纯文本来处理。

1
2
3
4
var xhr = createXHR();
xhr.open('get', 'test.php', true);
xhr.overrideMimeType('text/xml');
xhr.send(null);

这个例子强迫XHR对象将响应当作XML而非纯文本来处理。调用overrideMimeType()必须在send()方法之前,才能保证重写响应的MIME类型。

支持overrideMimeType()方法的浏览器有Firefox、Safari 4+、Opera10.5和Chrome。

3 进度事件

Progress Events规范是W3C的一个工作草案,定义了与客户端服务者普通信有关的事件。这些事件最早其实只针对XHR操作,但目前也被其他API借鉴。有以下6个进度事件。

  • loadstart:在接收到响应数据的第一个字节时触发。
  • progress:在接收响应期间持续不断地触发。
  • error:在请求发生错误时触发。
  • abort:在因为调用abort()方法而终止连接时触发。
  • load:在接收到完整的响应数据时触发。
  • loadend:在通信完成或者触发error、abort或load事件后触发。

每个请求都从触发loadstart事件开始,接下来是一或多个progress事件,然后触发error、abort或load事件中的一个,最后以触发loadend事件结束。
支持前5个事件的浏览器有Firefox 3.5+、Safari 4+、Chrome、iOS版Safari和Android版WebKit。Opera(从第11版开始)、IE8+只支持load事件。目前还没有浏览器支持loadend事件。

3.1 load事件

Firefox在实现XHR对象的某个版本时,曾致力于简化异步交互模式。最终,Firefox实现中引入了load事件,用以替代readystatechange事件。响应接收完毕后将触发load事件,因此也就没有必要去检查readyState属性了。而onload事件处理程序会接收到-个event对象,其target属性就指向XHR对象实例,因而可以访问到XHR对象的所有方法和属性。然而,并非所有浏览器都为这个事件实现了适当的事件对象。结果,开发人员还是要像下面这样被迫使用XHR对象变量。

1
2
3
4
5
6
7
8
9
10
11
12
var xhr = createXHR();
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300 ||
xhr.status == 304) {
alert(xhr.responseText);
} else {
alert('Request was unsuccessful: ' + xhr.status);
}
};

xhr.open('get', 'test.php', false);
xhr.send(null);

只要浏览器接收到服务器的响应,不管其状态如何,都会触发load事件。 而这意味着你必须要检查status属性,才能确定数据是否真的已经可用了。Firefox、Opera、Chrome和Safari都支持load事件。

3.2 progress事件

Mozilla对XHR的另一个革新是添加了progress事件,这个事件会在浏览器接收新数据期间周期性地触发。而onprogress事件处理程序会接收到一个event对象.其target属性是XHR对象,是一个表示进度信息是否可用的布尔值,position表示已经接收的字节数,totalSize表示根据Content-Length响应头部确定的预期字节数。有了这些信息,我们就可以为用户创建一个进度指示器了。下面展示了为用户创建进度指示器的-个示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var xhr = createXHR();
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300 ||
xhr.status == 304) {
alert(xhr.responseText);
} else {
alert('Request was unsuccessful: ' + xhr.status);
}
};

xhr.onprogress = function (event) {
var divStatus = document.getElementById('status');
if (event.lengthComputable) {
divStatus.innerHTML = 'Received ' + event.position + ' of ' + event.totalSize + ' bytes'
}
};

xhr.open('get', 'test.php', false);
xhr.send(null);

为确保正常执行,必须在调用open()方法之前添加onprogress事件处理程序。在前面的例子中,每次出发progress事件,都会以新的状态信息更新HTML元素的内容。如果响应头部中包含Content-Length字段,那么也可以利用此信息来计算从响应中已经接收到的数据的百分比。

4 跨源资源共享

通过XHR实现Ajax通信的一个主要限制,来源于跨域安全策略。默认情况下,XHR对象只能访问与包含它的页面位于同一个域巾的资源。这种安全策略可以预防某些恶意行为。但是,实现合理的跨域请求对开发某些浏览器应用程序也是至关重要的。
CORS(Cross-Origin Resource Sharing,跨源资源共享)是W3C的一个工作草案,定义了在必须访问跨源资源时,浏览器与服务器应该如何沟通。CORS背后的基本思想就是使用自定义的 HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。
比如一个简单的使用GET或POST发送的请求,它没有自定义的头部,而主体内容是text/plain。在发送该请求时,需要给它附加一个额外的Origin头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是-否给予响应。 下面是Origin头部的一个示例:

1
Origin: http://www.michealwayne.cn

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

1
Access-Control-Allow-Origin: http://www.michealwayne.cn

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

4.1 IE对CORS的实现

微软在IE8中引入了XDR(XDomainRequest)类型。 这个对象与XHR类似,但能实现安全可靠的跨域通信。XOR对象的安全机制部分实现了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请求都是异步执行的,不能用它来创建同步请求。请求返回之后.会触发load事件,响应的数据也会保存在responseText属性中.如下所示。

1
2
3
4
5
6
7
var xdr = new XDomainRequest();
xdr.onload = function () {
alert(xdr.responseText);
};

xdr.open('get', 'http://www.somewhere-else.com');
xdr.send(null);

在接收到响应后,你只能访问响应的原始文本;没有办法确定响应的状态代码。而且,只要响应有效就会触发load事件,如果失败(包括响应头少Access-Control-Allow-Origin头部)就会触发error事件。遗憾的是,除了错误本身之外,没有其他信息可用,因此唯一能够确定的就只有亲情未成功了。要检测错误,可以像下而这样指定一个onerror事件处理程序。

1
2
3
4
5
6
7
8
9
10
var xdr = new XDomainRequest();
xdr.onload = function () {
alert(xdr.responseText);
};
xdr.onerror = function () {
alert('An error occurred');
};

xdr.open('get', 'http://www.somewhere-else.com');
xdr.send(null);

鉴于导致XDR请求失败的因素很多,因此建议你不要忘记通过onerror事件处理程序来捕获该事件;否则,即使请求失败也不会有任何提示。

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

1
xdr.abort();

与XHR一样,XDR对象也支持timeout属性以及ontimeout事件处理程序。下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var xdr = new XDomainRequest();
xdr.onload = function () {
alert(xdr.responseText);
};
xdr.onerror = function () {
alert('An error occurred');
};
xdr.timeout = 1000;
xdr.ontimeout = function () {
alert('Request took too long');
};

xdr.open('get', 'http://www.somewhere-else.com');
xdr.send(null);

为支持POST请求,XDR对象提供了contentType属性。用来表示发送数据的格式, 如下面的例子所示。

1
2
3
4
5
6
7
8
9
10
11
var xdr = new XDomainRequest();
xdr.onload = function () {
alert(xdr.responseText);
};
xdr.onerror = function () {
alert('An error occurred');
};

xdr.open('post', 'http://www.somewhere-else.com');
xdr.contentType = 'application/x-www-form-urlencoded';
xdr.send('name=value1&time=2018');

这个属性是通过XDR对象影响头部信息的唯一方式。

4.2 其他浏览器对 CORS 的实现

Firefox 3.5+、Safari4+、Chrome、iOS版Safari和Android平台中的WebKit都通过XMLHttpRequest对象实现了对CORS的原生支持。在尝试打开不同来源的资源时,无需额外编写代码就可以触发这个行为。要请求位于另一个域中的资源,使用标准的XHR对象并在open()方法中传入绝对URL即可。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var xhr = createXHR();
xhr.onreadystatechange = function () {
if (xhr.readState === 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.somewhere-else.com', true);
xhr.send(null);

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

  • 不能使用setRequestHeader()设置自定义头部。
  • 不能发送和接收cookie。
  • 调用getAllResponseHeaders()方法总会返回空字符串。

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

4.3 Preflighted Reqeusts

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

  • Origin:与简单的请求相同。
  • Access-Control-Request-Method:请求自身使用的方法。
  • Access-Control-Request-Headers: (可选)自定义的头部信息,多个头部以逗号分隔。以下是一个带有自定义头部NCZ的使用POST方法发送的请求。
1
2
3
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请求援存多长时间(以秒表示)。

例如:

1
2
3
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请求。
支持Preflight请求的浏览器包括Firefox 3.5+、Safari 4+和Chrome。IE10及更早版本都不支持。

4.4 带凭据的请求

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

1
Access-Control-Allow-Credentials: true

如果发迭的是带凭据的请求,但服务器的响应中没有包含这个头部,那么浏览器就不会把响应交给JavaScript(于是,responseText中将是空字符串,status的值为0,而且会调用onerror()事件处理程序)。另外,服务器还可以在Preflight响应中发送这个Http头部,表示允许源发送带凭据的请求。
支待withCredentials属性的浏览器有Firefox 3.5+、Safari4+和Chrome。IE10及更早版本都不支持。

4.5 跨浏览器的 CORS

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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');
if (request) {
request.onload = function () {
// request.responseText;
};

request.send();
}

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

  • abort():用于停止正在进行的请求。
  • onerror:用于替代onreadystatechange检测错误。
  • onload:用于替代onreadystatechange检测成功。
  • responseText:用于取得响应内容。
  • send():用于发送请求。

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

5 其他跨域技术

在CORS出现以前,要实现跨域Ajax通信颇费一些周折。开发人员想出了一些办法,利用DOM中能够执行跨域请求的功能,在不依赖XHR对象的情况下也能发送某种请求。虽然CORS技术已经无处不在,但开发人员自己发明的这些技术仍然被广泛使用,毕竟这样不需要修改服务端代码。

5.1 图像Ping

上述第一种跨域请求技术是使用<img>标签。我们知道,一个网页可以从任何网页中加载图像,不用担心跨域不跨域。这也是在线广告跟踪浏览量的主要方式。正如此前讨论过的,也可以动态地创建图像,使用它们的onload和onerror事件处理程序来确定是否接收到了响应。
动态创建固像经常用于图像Ping。图像Ping是与服务器进行简单、单向的跨域通信的一种方式。请求的数据是通过查询字符串形式发送的,而响应可以是任意内容,但通常是像素图或204响应。通过图像Ping,浏览器得不到任何具体的数据,但通过侦听load和error事件,它能知道响应是什么时候接收到的。来看下面的例子。

1
2
3
4
5
var img = new Image();
img.onload = img.onerror = function () {
alert('Done');
};
img.src = 'http://www.somewhere-else.com';

这里创建了一个Image的实例,然后将onload和onerror事件处理程序指定为同一个函数。这样无论是什么响应,只要请求完成,就能得到通知。请求从设置src属性:那一刻开始,而这个例子在请求中发送了一个name参数。
图像Ping最常用于跟踪用户点击页面或动态广告曝光次数。Ping有两个主要的缺点,一是只能发送GET请求,二是无法访问服务端的响应文本。因此,因像Ping只能用于浏览器与服务端间的单向通信。

5.2 JSONP

JSONP是JSON with padding(填充式JSON或参数式JSON)的简写,是应用JSON的一种新方法,在后来的Web服务中非常流行。JSONP着跑来与JSON差不多.只不过是被包含;在函数调用中的JSON,就像下面这样。

1
callback({ 'name': 'Micheal' });

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

1
http://freegeoip.net/Json/?callback=handleResponse

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

1
2
3
4
5
6
7
function handleResponse (response) {
alert('You are 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之所以在开发人员中极为流行,主要原因是它非常简单易用。与图像Ping相比,它的优点在于能够直接访问响应文本,支持在浏览器与服务器之间双向通信。不过,JSONP也有两点不足。

  • 首先,JSONP是从其他域中加载代码执行。如果其他域不安全,很可能会在响应中夹带一些恶意代码,而此时除了完全放弃JSONP调用之外,没有办法追究。因此在使用不是你自己运维的Web服务时,一定得保证官安全可靠。
  • 其次,要确定JSONP请求是否失败并不容易。虽然HTML5给<script>元素新增了一个onerror事件处理程序,但目前还没有得到任何浏览器支持。为此,开发人员不得不使用计时器检测指定时间内是否接收到了响应。但就算这样也不能尽如人意,毕竟不是每个用户上网的速度和带宽都一样。

5.3 Comet

Comet是AlexRussell发明的一个词儿,指的是一种更高级的Ajax技术(经常也有人称为“服务器推迭”)。Ajax是一种从页面向服务器请求数据的技术,而Comet则是一种服务器向页面推送数据的技术。Comet能够让信息近乎实时地被推送到页面上,非常适合处理体育比赛的分数和股票报价。
有两种实现Comet的方式:长轮询和流。长轮询是传统轮询(也称为短轮询)的一个翻版,即浏览器定时向服务器发送请求,看有没有更新的数据。下图展示的是短轮询的时间线。

短轮询

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

长轮询

无论是短轮询还是长轮询.浏览器都要在接收数据之前,先发起对服务器的连接。两者最大的区别在于服务器如何发送数据。短轮询是服务器立即发送响应,无论数据是否有效,而长轮询是等待发送响应。轮询的优势是所有浏览器都支持,因为使用XHR对象和setTimeout()就能实现。而你要做的就是决定什么时候发送请求。
第二种流行的Comet实现是HTTP流。流不同于上述两种轮询,因为它在页面的整个生命周期内只使用一个HTIP连接。具体来说,就是浏览器向服务都发送一个请求,而服务器保持连接打开,然后周期性地向浏览器发送数据。比如,下面这段PHP脚本就是采用流实现的服务帮中常见的形式。

1
2
3
4
5
6
7
8
9
10
<?php
$i = 0;
while(true) {
echo "Number is $i";
flush();

sleep(10);

$i++;
}

所有服务器端语言都支持打印到输出缓存然后刷新(将输出缓存中的内容一次性全部发送到客户端)的功能。而这正是实现HTTP流的关键所在。
在Firefox、Safari、Opera和Chrome中,通过侦听readystatechange事件及检测readyState的值是否为3,就可以利用XHR对象实现HTTP流。 在上述这些浏览器中,随着不断从服务器接收数据,readyState的值会周期性地变为3。当readyState值变为3时,responseText属性中就会保存接收到的所有数据。此时,就需要比较此前接收到的数据。决定从什么位置开始取得最新的数据。使用XHR对象实现HTTP流的典型代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function createStreamingClient(url, process, finished) {
var xhr = new XMLHttpRequest();
var 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回调函数
process(result);
} else if (xhr.readState === 4) {
finished(xhr.responseText);
}
};

xhr.send(null);
return xhr;
}

// use
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网调函数,传入响应返回的全部内容。
虽然这个例子比较简单,而且也能在大多数浏览器中正常运行(IE除外),但管理Comet的连接是很容易出错的,需要时间不断改进才能达到完美。浏览器社区认为Comet是未来Web的一个重要组成部分。为了简化这一技术,又为Comet创建了两个新的接口。

5.4 服务器发送事件

SSE(Server-Sent Events,服务器发送事件)是围绕只读Comet交互推出的API或者模式。SSE API 用于创建到服务器的单向连接,服务器通过这个连接可以发送任意数量的数据。 服务然响应的MIME类型必须是text/event-stream,而且是浏览器中的JavaScriptAPI能解析格式输出。SSE支持短轮询长轮询和HTTP流,而且能在断开连接时自动确定何时重新连接。有了这么简单实用的API,再实现Comet就容易多了。

支持SSE的浏览器有Firefox6+、Safari 5+、Opera 11+、Chrome和iOS4+版Safari。

1. SSE API

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

1
var source = new EventSource('event.php');

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

  • open;在建立连接时触发。
  • message:在从服务然接收到新事件时触发。
  • error:在无法建立连接时触发。
    就一般的用法而言,onmessage事件处理程序,也没有什么特别的。
1
2
3
4
5
source.onmessage = function (event) {
var data = event.data;

// 处理data
}

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

1
source.close();

2.事件流

所谓的服务器事件会通过一个持久的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”,第三个返回为”foo\nbar”。对于多个连续的以data:开头的数据行,将作为多段数据解析,每个值之间以一个换行符分隔。只有在包含data:的数据行后面有空行时,才会触发message事件,因此在服务器上生成事件流时不能忘了多添加这一行。

通过id:前缀可以给特定的事件指定一个关联的ID,这个ID行位于data:行前面或后面皆可:

1
2
data: foo
id: 1

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

5.5 Web Sockets

要说最令人津津乐道的新浏览器API,就得数Web Sockets了。Web Sockets的目标是在-个单独的持久连接上提供全双工、双向通信。在JavaScript中创建了Web Socket之后,会有一个HTTP请求发送到浏览器以发起连接。在取得服务器响应后,建立的连接会使用HTTP升级从HTTP协议交换为Web Socket协议。也就是说使用标准的HTTP服务器无法实现Web Sockets,只时这种协议的专门服务器才能正常工作。
由于Web Sockets使用了自定义的协议,所以URL模式也略有不同。未加密的连接不再是http://,而是ws://;加密的连接也不是https://,而是wss://。在使用Web Socket URL时,必须带着这个模式,因为将来还有可能支持其他模式。

使用自定义协议而非HTTP协议的好处是,能够在客户端和服务器之间发送非常少量的数据,而不必担心HTTP那样字节级的开销。由于传递的数据包很小,因此Web Sockets非常适合移动应用。毕竟对移动应用而言,带宽和网络延迟都是关键问题。使用自定义协议的缺点在于,制定协议的时间比制定JavaScript API的时间还要长。Web Sockets曾几度搁浅,就因为不断有人发现这个新协议存在一致性和安全性的问题。Firefox4和Opera11 都曾默认启用Web Socket,但在发布前夕又禁用了,因为又发现了安全隐患。目前支持Web Socket的浏览器有Firefox6+、Safari5+、Chrome和iOS4+版Safari。

1 Web Sockets API

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

1
var socket = new WebSocket('ws://www.michealway.cn/test.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()方法。

1
socket.close();

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

发送和接收数据

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

1
2
var socket = new WebSocket('ws://www.michealway.cn/test.php');
socket.send('Hello');

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

1
2
3
4
socket.send(JSON.stringify({
name: 'Micheal',
age: 24
}));

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

1
2
3
4
5
socket.onmessage = function (event) {
var data = event.data;

// 处理data
}

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

3.其他事件

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

  • open:在成功建立连接时触发。
  • error:在发生错误时触发。连接不能持续。
  • close:在连接关闭时触发。
    WebSocket对象不支持DOM 2级事件侦听器,因此必须使用DOMO级语法分别定义每个事件处理程序。
1
2
3
4
5
6
7
8
9
10
11
12
var socket = new WebSocket('ws://www.michealway.cn/test.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是一个字符串,包含服务器发回的消息。可以把这些信息显示给用户,也可以记录到日志中以便将来分析。

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

Web Sockets兼容如下图所示:

Web Socket

5.6 SSE与WebSockets

面对某个具体的用例,在考虑是使用SSE还是使用WebSockets时,可以考虑如下几个因素。首先,你是否有自由度建立和维护WebSockets服务器?因为WebSocket协议不同于HTTP,所以现有服务器不能用于WebSocket通信。SSE倒是通过常规Hπp通信,因此现有服务器就可以满足需求。
第二个要考虑的问题是到底需不需要双向通信。如果用倒只需读取服务器数据(如比赛成绩),那么SSE比较容易实现。如果用例必须双向通信(如聊天室),那么WebSockets显然更好。别忘了,在不能选择WebSockets的情况下,组合XHR和SSE也是能实现双向通信的。

6 安全

讨论Ajax和Comet安全的文意可谓连篇累牍(~文化了),而相关主题的书也已经出了很多本了。大型Ajax应用程序的安全问题涉及面非常之广,但我们可以从普遍意义上探讨一些基本的问题。
首先,可以通过XHR访问的任何URL也可以通过浏览器或服务器来访问。下面的URL就是一个例子。

1
/getuserinfo.php?id=23

如果是向这个URL发送请求,可以想象结果会返回id为23的用户的某些数据。谁也无法保证别人不会将这个URL的用户ID修改为24、56或其他值。因此,getuserinfo.php文件必须知道请求者是否真的有权限访问要请求的数据;否则,你的服务器就会门户大开,任何人的数据都可能被泄漏出去。 对于未被授权系统有权访问某个资源的情况,我们称之为CSRF(Cross-Site Request Forgery,跨站点请求伪造)。未被授权系统会伪装自己,让处理请求的服务器认为它是合法的。受到CSRF攻击的Ajax程序有大有小,攻击行为既有旨在揭示系统漏洞的恶作剧,也有恶意的数据窃取或数据销毁。
为确保通过XHR访问的URL安全,通行的做法就是验证发送请求者是否有权限访问相应的资源。有下列几种方式可供选择。

  • 要求以SSL连接采访问可以通过XHR请求的资源。
  • 要求每一次请求都要附带经过相应算法计算得到的验证码。请注意,下列措施对防范CSRF攻击不起作用。
  • 要求发送 POST 而不是 GET 请求——很容易改变。
  • 检查来源URL以确定是否可信——来源记录很容易伪造。
  • 基于cookie 信息进行验证——同样很容易伪造。

XHR对象也提供了一些安全机制,虽然表面上看可以保证安全,但实际上却相当不可靠。实际上,前面介绍的open()方法还能再接收两个参数:要随请求一起发送的用户名和密码。带有这两个参数的请求可以通过SSL发送给服务器上的页面, 如下面的例子所示。

1
2
// don't do this
xhr.open('get', 'test.php', true, 'username', 'password');

即使可以考虑这种安全机制,但还是尽量不要这样做。把用户名和密码保存在
JavaScript代码中本身就是极为不安全的。任何人,只要他会使用JavaScript调试器,就可以通过查看相应的变量发现纯文本形式的用户名和密码。

温习:

  • XMLHttpRequest对象(XHR)介绍、用法、GET/POST请求
  • XMLHttpRequest2级(FormData、timeout、overrideMimeType)
  • 进度事件
  • CORS
  • 图像Ping
  • JSONP
  • Comet
  • SSE
  • Web Sockets
  • 发请求前考虑安全性

(完)