跨域请求一直是网页编程中的一个难题,在过去,绝大多数人都倾向于使用JSONP来解决这一问题。不过现在,我们可以考虑一下W3C中一项新的特性——CORS(Cross-Origin Resource Sharing)了。

本文的所有代码均来自http://www.html5rocks.com/en/tutorials/cors/,如果您对其中的任何技术细节存在疑问,请以原文为准。

客户端

创建XmlHttpRequest对象

对于CORS,Chrome、FireFox以及Safari,需要使用XmlHttpRequest2对象;而对于IE,则需要使用XDomainRequest;Opera目前还不支持这一特性,但很快就会支持。

因此,在对象的创建上,我们不得不首先针对不同的浏览器而进行一下预处理:

function createCORSRequest(method, url) {
var xhr = new XMLHttpRequest();
if ("withCredentials" in xhr) { // "withCredentials"属性是XMLHTTPRequest2中独有的
xhr.open(method, url, true); } else if (typeof XDomainRequest != "undefined") { // 检测是否XDomainRequest可用
xhr = new XDomainRequest();
xhr.open(method, url); } else { // 看起来CORS根本不被支持
xhr = null; }
return xhr;
} var xhr = createCORSRequest('GET', url);
if (!xhr) {
throw new Error('CORS not supported');

事件处理

原先的XmlHttpRequest对象仅仅只有一个事件——onreadystatechange,用以通知所有的事件,而现在,我们除了这个事件之外又多了很多新的。

事件 说明
onloadstart* 当请求发生时触发
onprogress 读取及发送数据时触发
onabort* 当请求被中止时触发,如使用abort()方法
onerror 当请求失败时触发
onload 当请求成功时触发
ontimeout 当调用者设定的超时时间已过而仍未成功时触发
onloadend* 请求结束时触发(无论成功与否)

注:带星号的表示IE的XDomainRequest仍不支持。
数据来自http://www.w3.org/TR/XMLHttpRequest2/#events

绝大多数情况下,我们只需要和onloadonerror打交道,就像下面这样:

xhr.onload = function() {

var responseText = xhr.responseText;

 console.log(responseText);
// 继续其它代码
}; xhr.onerror = function() {
console.log('There was an error!');
};

这儿有一点小异样。尽管我们可以通过onerror得知请求发生了错误,但在事件处理时,我们无法从代码上获知失败的任何原因。比如,FireFox在失败时将responseText置空并返回一个0值作为状态,这当中并不包含任何错误的具体情况。

withCredentials

标准的CORS请求不对cookies做任何事情,既不发送也不改变。如果希望改变这一情况,就需要将withCredentials设置为true 

xhr.withCredentials = true; 

另外,服务端在处理这一请求时,也需要将Access-Control-Allow-Credentials设置为true。这一点我们稍后来说。

withCredentials属性使得请求包含了远程域的所有cookies,但值得注意的是,这些cookies仍旧遵守“同域”的准则,因此从代码上你并不能从document.cookies或者回应HTTP头当中进行读取。

发送请求

请求通过一个简单的send()方法进行发送,如果请求当中需要包含任何内容,也只需要将其作为一个参数传递给send()即可。一旦服务端配置OK,那么你将只需要处理后续的onload事件,这正像我们平时所熟悉的XHR一样。

来看一段完整的小代码: 

// 创建XHR对象
function createCORSRequest(method, url) {
var xhr = new XMLHttpRequest();
if ("withCredentials" in xhr) {
// 针对Chrome/Safari/Firefox.
xhr.open(method, url, true);
} else if (typeof XDomainRequest != "undefined") {
// 针对IE
xhr = new XDomainRequest();
xhr.open(method, url);
} else {
// 不支持CORS
xhr = null;
}
return xhr;
} // 辅助函数,用于解析返回的内容
function getTitle(text) {
return text.match('')[1];
} // 发送CORS请求
function makeCorsRequest() {
// bibliographica.org是支持CORS的
var url = 'http://bibliographica.org/'; var xhr = createCORSRequest('GET', url);
if (!xhr) {
alert('CORS not supported');
return;
} // 回应处理
xhr.onload = function() {
var text = xhr.responseText;
var title = getTitle(text);
alert('Response from CORS request to ' + url + ': ' + title);
}; xhr.onerror = function() {
alert('Woops, there was an error making the request.');
}; xhr.send();
}

服务端

一个CORS请求可能包含多个HTTP头,甚至有多个请求实际发送,这对于客户端的开发者来说通常是透明的。因为浏览器已经负责实现了CORS最关键的部分;但是服务端的后台脚本则需要我们自己进行处理,因此我们还需要了解到服务端到底从浏览器那里收到了怎样的内容。

先来看看流程图吧。

CORS分类

CORS可以分成两种:

  • 简单请求
  • 复杂请求

一个简单的请求大致如下:

  • HTTP方法是下列之一

    • HEAD
    • GET
    • POST
  • HTTP头包含
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type,但仅能是下列之一
      • application/x-www-form-urlencoded
      • multipart/form-data
      • text/plain

任何一个不满足上述要求的请求,即被认为是复杂请求。一个复杂请求不仅有包含通信内容的请求,同时也包含预请求(preflight request)。

简单请求

为了搞清楚复杂请求与简单请求有何区别,我们首先来看看简单请求是怎样处理的。

JavaScript:

var url = 'http://alice.com/cors';
var xhr = createCORSRequest('GET', url);
xhr.send();

HTTP请求: 

GET /cors HTTP/1.1
Origin: http://api.alice.com
Host: api.bob.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

简单请求的发送从代码上来看和普通的XHR没太大区别,但是HTTP头当中要求总是包含一个域(Origin)的信息。该域包含协议名、地址以及一个可选的端口。不过这一项实际上由浏览器代为发送,并不是开发者代码可以触及到的。

HTTP回应:

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

在回应中,COR相关的项目全都是以“Access-Control-”作为前缀的,其意义分列如下:

  • Access-Control-Allow-Origin(必含)- 不可省略,否则请求按失败处理。该项控制数据的可见范围,如果希望数据对任何人都可见,可以填写“*”。
  • Access-Control-Allow-Credentials(可选) – 该项标志着请求当中是否包含cookies信息,只有一个可选值:true(必为小写)。如果不包含cookies,请略去该项,而不是填写false。这一项与XmlHttpRequest2对象当中的withCredentials属性应保持一致,即withCredentialstrue时该项也为truewithCredentialsfalse时,省略该项不写。反之则导致请求失败。
  • Access-Control-Expose-Headers(可选) – 该项确定XmlHttpRequest2对象当中getResponseHeader()方法所能获得的额外信息。通常情况下,getResponseHeader()方法只能获得如下的信息:
    • Cache-Control
    • Content-Language
    • Content-Type
    • Expires
    • Last-Modified
    • Pragma

    当你需要访问额外的信息时,就需要在这一项当中填写并以逗号进行分隔。不过目前浏览器对这一项的实现仍然有一些问题,具体请见文尾的BUG一节。

复杂请求

如果仅仅是简单请求,那么即便不用CORS也没有什么大不了,但CORS的复杂请求就令CORS显得更加有用了。简单来说,任何不满足上述简单请求要求的请求,都属于复杂请求。比如说你需要发送PUTDELETE等HTTP动作,或者发送Content-Type: application/json的内容。

复杂请求表面上看起来和简单请求使用上差不多,但实际上浏览器发送了不止一个请求。其中最先发送的是一种“预请求”,此时作为服务端,也需要返回“预回应”作为响应。预请求实际上是对服务端的一种权限请求,只有当预请求成功返回,实际请求才开始执行。

JavaScript:

var url = 'http://alice.com/cors';
var xhr = createCORSRequest('PUT', url);
xhr.setRequestHeader(
'X-Custom-Header', 'value');
xhr.send();

预请求:

OPTIONS /cors HTTP/1.1
Origin: http://api.alice.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.bob.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

预请求以OPTIONS形式发送,当中同样包含域,并且还包含了两项CORS特有的内容:

  • Access-Control-Request-Method – 该项内容是实际请求的种类,可以是GET、POST之类的简单请求,也可以是PUTDELETE等等。
  • Access-Control-Request-Headers – 该项是一个以逗号分隔的列表,当中是复杂请求所使用的头部。

显而易见,这个预请求实际上就是在为之后的实际请求发送一个权限请求,在预回应返回的内容当中,服务端应当对这两项进行回复,以让浏览器确定请求是否能够成功完成。例如,刚才的预请求可能获得服务端如下的回应:

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8

来看看预回应当中可能的项目:

  • Access-Control-Allow-Origin(必含) – 和简单请求一样的,必须包含一个域。
  • Access-Control-Allow-Methods(必含) – 这是对预请求当中Access-Control-Request-Method的回复,这一回复将是一个以逗号分隔的列表。尽管客户端或许只请求某一方法,但服务端仍然可以返回所有允许的方法,以便客户端将其缓存。
  • Access-Control-Allow-Headers(当预请求中包含Access-Control-Request-Headers时必须包含) – 这是对预请求当中Access-Control-Request-Headers的回复,和上面一样是以逗号分隔的列表,可以返回所有支持的头部。
  • Access-Control-Allow-Credentials(可选) – 和简单请求当中作用相同。
  • Access-Control-Max-Age(可选) – 以秒为单位的缓存时间。预请求的的发送并非免费午餐,允许时应当尽可能缓存。

一旦预回应如期而至,所请求的权限也都已满足,则实际请求开始发送。

实际请求:

PUT /cors HTTP/1.1
Origin: http://api.alice.com
Host: api.bob.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

实际回应:

Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

如果预请求所要求的权限服务端不允许,那么服务端可以直接返回一个普通的HTTP回应,比如:

// ERROR - No CORS headers, this is an invalid request!
Content-Type: text/html; charset=utf-8

这样的返回因为不符合客户端的需求,因而客户端会直接将请求以失败计,虽然不是很美气,不过正符合我们的实际。此时如果客户端的onerror事件有监听函数,那么将会触发,而浏览器的console窗口也会输出:

XMLHttpRequest cannot load http://api.alice.com. Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

不过很可惜,浏览器并不会给出详细的错误情况,仅仅是告知我们出错而已。

安全问题

跨域请求始终是网页安全中一个比较头疼的问题,CORS提供了一种跨域请求方案,但没有为安全访问提供足够的保障机制,如果你需要信息的绝对安全,不要依赖CORS当中的权限制度,应当使用更多其它的措施来保障,比如OAuth2。

已知问题

CORS是W3C中一项较“新”的方案,以至于各大网页解析引擎还没有对其进行完美的实现。下面是截至2011年11月13日时的已知问题:

  • getAllResponseHeaders()方法无法获取Access-Control-Expose-Headers当中要求的信息。在Chrome/Safari当中,仅仅只有简单的头部能够读取,其他无法获取;在FireFox当中,无法获得任何信息。(FireFox Bugzilla/Webkit Bugzilla
  • 在Safari当中,使用GETPOST方法的复杂请求发送时没有发送预请求的环节。
  • onerror触发时statusText获取不到任何内容。
  • Opera截至11.60仍旧不支持CORS,但在12当中会支持(Opera Core Concerns – CORS goes mainline)。

CORS 跨域的更多相关文章

  1. 如何在ASP.NET Core中实现CORS跨域

    注:下载本文的完整代码示例请访问 > How to enable CORS(Cross-origin resource sharing) in ASP.NET Core 如何在ASP.NET C ...

  2. ajax——CORS跨域调用REST API 的常见问题以及前后端的设置

    RESTful架构是目前比较流行的一种互联网软件架构,在此架构之下的浏览器前端和手机端能共用后端接口. 但是涉及到js跨域调用接口总是很头疼,下边就跟着chrome的报错信息一起来解决一下. 假设:前 ...

  3. Web APi之手动实现JSONP或安装配置Cors跨域(七)

    前言 照理来说本节也应该讲Web API原理,目前已经探讨完了比较底层的Web API消息处理管道以及Web Host寄宿管道,接下来应该要触及控制器.Action方法,以及过滤器.模型绑定等等,想想 ...

  4. Web API 实现JSONP或者安装配置Cors跨域

    前言 照理来说本节也应该讲Web API原理,目前已经探讨完了比较底层的Web API消息处理管道以及Web Host寄宿管道,接下来应该要触及控制器.Action方法,以及过滤器.模型绑定等等,想想 ...

  5. CORS 跨域 实现思路及相关解决方案

    本篇包括以下内容: CORS 定义 CORS 对比 JSONP CORS,BROWSER支持情况 主要用途 Ajax请求跨域资源的异常 CORS 实现思路 安全说明 CORS 几种解决方案 自定义CO ...

  6. 解决ajax请求cors跨域问题

    ”已阻止跨源请求:同源策略禁止读取位于 ***** 的远程资源.(原因:CORS 头缺少 'Access-Control-Allow-Origin').“ ”已阻止跨源请求:同源策略禁止读取位于 ** ...

  7. Asp.Net WebApi 启用CORS跨域访问指定多个域名

    1.后台action指定 EnableCors指定可访问的域名多个,使用逗号隔开 //支持客户端凭据提交,指定多个域名,使用逗号隔开 [EnableCors("http://localhos ...

  8. Cors 跨域Access-Control-Allow-Origin

    1.Access-Control-Allow-Origin 指定格式 The Origin header field has the following syntax: origin = " ...

  9. Asp.Net WebApi+Microsoft.AspNet.WebApi.Core 启用CORS跨域访问

    WebApi中启用CORS跨域访问 1.安装 Nugget包Microsoft.AspNet.WebApi.Cors This package contains the components to e ...

  10. jsonp与cors跨域的一些理解

    浏览器的同源策略,即是浏览器之间要隔离不同域的内容,禁止互相操作. 比如,当你打开了多个网站,如果允许多个网站之间互相操作,那么其中一个木马网站就可以通过这种互相操作进行一系列的非法行为,获取你在各个 ...

随机推荐

  1. 一切不以用户为中心的O2O 都是耍流氓

    今天去万达广场逛街,手机搜了下附近的Wifi,发现有万达的免费Wifi,想起前些日子网上说万达要做O2O的试运营,于是尝试连接了下,结果弹出页面,要输入手机号,然后发送验证码才能登陆,结果输入手机号, ...

  2. Intra Refresh of H264 encoder

    https://en.wikipedia.org/wiki/X264 x264 is able to use Periodic Intra Refresh instead of keyframes, ...

  3. hdu3315-My Brute(费用流 or KM算法)

    题目:My Brute Seaco是一个漂亮的妹子,喜欢玩一款名叫My Brute的游戏.情人节快到了,starvae和xingxing都想邀请妹子过节,但是妹子只能陪一个啊,于是两个人决定打一架,用 ...

  4. Java网络编程(TCP客户端)

    TCP传输:两个端点建立连接后会有一个传输数据的通道,这个通道就称为流,而且是建立在网络基础上的流,之为socket流,该流中既可以读取也可以写入. TCP的两个端点:一个客户端:ServerSock ...

  5. 单机c/s软件如何让老板在异地看销售营业报表

    单机软件,让人的感觉就只能在本地使用. 单机版c/s软件,数据存放在本机上,老板想要查看销售报表的话,需要跑到公司的那台电脑上才能查看,这对于在外面四处跑业务的老板来说,基本上是不可能做到的.但每天的 ...

  6. [OC Foundation框架 - 21] NSSet集合 & 集合之间的转换

    A.NSSet 跟NSArray一样,不可变 NSArray 自然顺序 NSSet是无序的 NSSet不允许存入重复元素,可以用来过滤重复元素   也有可变的NSMutableSet   B.集合转换 ...

  7. pom.xml报错

    错误太多打开文件后就不会显示,点开前边的选项卡就会提示错误信息,一般是jar包没加载全.

  8. 虚方法(virtual)和抽象方法(abstract)的区别

    注:本文转载自 http://www.cnblogs.com/michaelxu/archive/2008/04/01/1132633.html 虚方法和抽象方法都可以供派生类重写,它们之间有什么区别 ...

  9. [一位菜鸟的COCOS-2D编程之路]打飞机中机种敌机和战机损毁时的爆炸效果

    1.第一步,添加爆炸动画 //添加玩家飞机飞行动画 id _playerFlyAction; id _playerBlowupAnimation; //战机爆炸动画 id _enemyBlowupAn ...

  10. 如何检查机器是否因为装了Windows更新而需要重新启动

    博客搬到了fresky.github.io - Dawei XU,请各位看官挪步.最新的一篇是:如何检查机器是否因为装了Windows更新而需要重新启动.