21.4 跨源资源共享

优质
小牛编辑
115浏览
2023-12-01
通过XHR 实现Ajax 通信的一个主要限制,来源于跨域安全策略。默认情况下,XHR 对象只能访问与包含它的页面位于同一个域中的资源。这种安全策略可以预防某些恶意行为。但是,实现合理的跨域请求对开发某些浏览器应用程序也是至关重要的。 CORS(Cross-Origin Resource Sharing,跨源资源共享)是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 信息。

21.4.1 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 请求都是异步执行的,不能用它来创建同步请求。请求返回之后,会触发load 事件,响应的数据也会保存在responseText 属性中,如下所示。
var xdr = new XDomainRequest();
xdr.onload = function() {
alert(xdr.responseText);
};
xdr.open("get", "http://www.somewhere-else.com/page/");
xdr.send(null);
在接收到响应后,你只能访问响应的原始文本;没有办法确定响应的状态代码。而且,只要响应有效就会触发load 事件,如果失败(包括响应中缺少Access-Control-Allow-Origin 头部)就会触发error 事件。遗憾的是,除了错误本身之外,没有其他信息可用,因此唯一能够确定的就只有请求未成功了。要检测错误,可以像下面这样指定一个onerror 事件处理程序。
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/page/");
xdr.send(null);
运行一下
鉴于导致XDR 请求失败的因素很多,因此建议你不要忘记通过onerror 事件处理程序来捕获该事件;否则,即使请求失败也不会有任何提示。
在请求返回前调用abort()方法可以终止请求:
xdr.abort(); //终止请求
与XHR 一样,XDR 对象也支持timeout 属性以及ontimeout 事件处理程序。下面是一个例子。
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/page/");
xdr.send(null);
这个例子会在运行1 秒钟后超时,并随即调用ontimeout 事件处理程序。 为支持POST 请求,XDR 对象提供了contentType 属性,用来表示发送数据的格式,如下面的例子所示。
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/page/");
xdr.contentType = "application/x-www-form-urlencoded";
xdr.send("name1=value1&name2=value2");
这个属性是通过XDR 对象影响头部信息的唯一方式。

21.4.2 其他浏览器对CORS的实现

Firefox 3.5+、Safari 4+、Chrome、iOS 版Safari 和Android 平台中的WebKit 都通过XMLHttpRequest对象实现了对CORS 的原生支持。在尝试打开不同来源的资源时,无需额外编写代码就可以触发这个行为。要请求位于另一个域中的资源,使用标准的XHR 对象并在open()方法中传入绝对URL 即可,例如:
var xhr = createXHR();
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.somewhere-else.com/page/", true);
xhr.send(null);
与IE 中的XDR 对象不同,通过跨域XHR 对象可以访问status 和statusText 属性,而且还支持同步请求。跨域XHR 对象也有一些限制,但为了安全这些限制是必需的。以下就是这些限制。
  • 不能使用setRequestHeader()设置自定义头部。
  • 不能发送和接收cookie。
  • 调用getAllResponseHeaders()方法总会返回空字符串。
由于无论同源请求还是跨源请求都使用相同的接口,因此对于本地资源,最好使用相对URL,在访问远程资源时再使用绝对URL。这样做能消除歧义,避免出现限制访问头部或本地cookie 信息等问题。

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

21.4.4 带凭据的请求

默认情况下,跨源请求不提供凭据(cookie、HTTP 认证及客户端SSL 证明等)。通过将withCredentials 属性设置为true,可以指定某个请求应该发送凭据。如果服务器接受带凭据的请求,会用下面的HTTP 头部来响应。
Access-Control-Allow-Credentials: true
如果发送的是带凭据的请求,但服务器的响应中没有包含这个头部,那么浏览器就不会把响应交给JavaScript(于是,responseText 中将是空字符串,status 的值为0,而且会调用onerror()事件处理程序)。另外,服务器还可以在Preflight 响应中发送这个HTTP 头部,表示允许源发送带凭据的请求。 支持withCredentials 属性的浏览器有Firefox 3.5+、Safari 4+和Chrome。IE 10 及更早版本都不支持。

21.4.5 跨浏览器的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() {//对request.responseText 进行处理
};
request.send();
}
运行一下 Firefox、Safari 和Chrome 中的XMLHttpRequest 对象与IE 中的XDomainRequest 对象类似,都提供了够用的接口,因此以上模式还是相当有用的。这两个对象共同的属性/方法如下。
  • abort():用于停止正在进行的请求。
  • onerror:用于替代onreadystatechange 检测错误。
  • onload:用于替代onreadystatechange 检测成功。
  • responseText:用于取得响应内容。
  • send():用于发送请求。
以上成员都包含在createCORSRequest()函数返回的对象中,在所有浏览器中都能正常使用。