CORS详解[译]

介绍

由于同源策略的缘故,以往我们跨域请求,会使用诸如JSON-P(不安全)或者代理(设置代理和维护繁琐)的方式。而跨源资源共享(Cross-Origin Resource Sharing)是一个W3C规范,其建立在XMLHttpRequest对象之上,允许开发人员像使用同源请求一样的规则,在浏览器端发送跨域请求。

CORS的使用场景很简单。例如,站点bob.com想要请求获取alice.com的数据,由于同源策略缘故,这种情况在传统请求中是不被允许的。然而,bob.com通过CORS请求alice.com,并在alice.com响应头中添加少许特殊的响应头,就可以达到bob.com获取到alice.com数据的目的。

正如你上面看到的例子,要实现CORS,需要客户端和服务端的共同协调。幸运的是,如果你是客户端开发人员,很多具体细节对于你来说是屏蔽的。好了,接下来我们将介绍客户端怎样发起跨域请求,以及服务端如何设置,从而达到支持CORS的目的。

发起一个CORS请求

该小节讲解了如何使用JavaScript发起一个跨域请求。

-创建XMLHttpRequest对象-

浏览器支持CORS情况,如下:

.Chrome 3+

.Firefox 3.5+

.Opera 12+

.Safari 4+

.Internet Explorer 8+

浏览器支持CORS情况,更多见http://caniuse.com/#search=cors

Chrome,Firefox,Opera 和 Safari都是使用XMLHttpRequest2对象。Internet Explorer使用了类似的对象XDomainRequest,其工作原理和XMLHttpRequest大致相同,但增加了额外的安全防范措施。

由于浏览器的差异,首先,你需要根据浏览器的不同,创建一个合适的请求对象。Nicholas Zakas写了一个简单的辅助方法,来屏蔽掉浏览器的差异,如下:

function createCORSRequest(method, url){
    var xhr = new XMLHttpRequest();
    if("withCredentials" in xhr){
        //检查XHLHttpRequest对象是否有"withCredentials"属性
        //"withCredentials"属性仅存在于XMLHttpReqeust2对象中
        xhr.open(method, url, true);
    }else if(typeof XDomainRequest !="undefined"){
        //否则,检查XDomainRequest
        //XDomainRequest仅存在IE中,且通过其发起CORS请求
        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,处理所有的响应。虽然onreadystatechange仍然可用,但是XMLHttpRequest2引入了更多新的事件句柄,如下:


事件句柄


描述


onloadstart*


当请求发起时


onprogress


当加载和发送数据时


onabort*


当请求被中断时。例如,调用abort()方法


onerror


当请求失败时


onload


当请求成功时


ontimeout


当请求时间超过开发者设定时间时


onloadend*


当请求完成时(成功或失败)

上述,凡是带有星号(*)的事件句柄,IE的XDomainRequest都不支持。

来源:http://www.w3.org/TR/XMLHttpRequest2/#events

在大多数情况下,我们至少会使用onload和onerror事件:

xhr.onload = function(){
    var responseText = xhr.responseText;
    console.log(responseText);
    //处理响应
};
xhr.onerror = function(){
    console.log(‘There was an error!‘);
}

当请求出现错误时,浏览器并不能很友好地报告出具体的错误。比如,Firefox对所有的错误都会报告0状态和空状态文本。浏览器也能通过日志反馈错误信息,但是信息却不能被JavaScript获取。当处理onerror事件句柄时,你会知道有错误出现,除此之外,一无所获。

-withCredentials-

标准的CORS请求,默认情况下是不会发送或者设置cookie值的。为了在请求时,附带cookies,我们需要设置XMLHttpRequest的withCredentials属性为true:

xhr.withCredentials = true;

为了让其运作,服务端也必须在响应头中设置Access-Control-Allow-Credentials为true,开启credentials。如下:

Access-Control-Allow-Credentials: true;

设置withCredentials属性后,远程域请求时会带上所有cookies,以及设置它们。注意,这些cookie值仍然遵守同源策略,所以我们的JavaScript代码仍然不能从document.cookie或者响应头中获取cookie,它们仅仅被远程域控制。

-发送请求-

现在我们的CORS请求设置完毕,我们通过调用send()方法,即可发起该请求,如下:

xhr.send();

如果该请求有请求体,那么作为send方法中的参数,发送即可。

客户端的CORS就这样啦!假设服务端已经设置好了CORS,当服务端返回响应后,我们的onload事件句柄就会被触发,就像你熟悉的标准同源XHR请求一样。

-端到端例子-

下面就是一个完整的CORS示例。运行示例并在浏览器调试器中查看实际请求操作。

// 创建XHR 对象.
function createCORSRequest(method, url) {
  var xhr = new XMLHttpRequest();
  if ("withCredentials" in xhr) {
    // XHR for Chrome/Firefox/Opera/Safari.
    xhr.open(method, url, true);
  } else if (typeof XDomainRequest != "undefined") {
    // XDomainRequest for IE.
    xhr = new XDomainRequest();
    xhr.open(method, url);
  } else {
    // 不支持CORS.
    xhr = null;
  }
  return xhr;
}

// 辅助函数:解析响应内容中的title标签
function getTitle(text) {
  return text.match(‘<title>(.*)?</title>‘)[1];
}

// 发起CORS请求.
function makeCorsRequest() {
  // HTML5 Rocks支持 CORS.
  var url = ‘http://updates.html5rocks.com‘;

  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

CORS最繁重的处理是在浏览器和服务器之间。当浏览器发送一个CORS请求时,会添加一些额外的响应头,有时还会发送额外的请求。这些额外的步骤对于客户端人员来说,是透明的(但是我们可以通过一个包分析器去发现,例如Wireshark)。

浏览器制造商负责浏览器端的实现。该小节将阐述,服务端如何设置它的头部,从而达到支持CORS的目的。

-CORS请求类型-

跨域请求有两种形式:

1、  简单请求

2、  非简单请求

简单请求满足以下条件:

.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

简单请求的特征如上所诉,因为它们不需要使用CORS就可以在浏览器中发起跨域请求了。例如,JSON-P发起GET请求跨域,又如HTML利用POST提交表单。

其他任何请求,只要不满足以上条件的,都是非简单请求,且发起非简单请求时,在浏览器和服务器之间需要额外的通信(又叫预请求)。好了,下面我们就一同进入跨域之旅吧。

-处理一个简单请求-

我们从客户端发起一个简单请求开始。下面的代码展示了如何利用JavaScript发起一个简单请求GET,以及浏览器实际发出的HTTP请求。

JavaScript:

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

HTTP请求:

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

值得注意的是,一个有效的CORS请求,总是包含一个Origin头部,而这个Origin头部又是浏览器自动添加的,用户操作不了。且,这个Origin头部的值是由协议(例如http),域名(例如bob.com)和端口(仅当不是默认端口时,包含,例如81)组成,如http://api.alice.com。

但也要注意,如果一个请求包含Origin头部,未必就是一个跨域请求。虽然所有的CORS请求都会包含一个Origin头部,但是一些同源请求可能也会包含它。例如,Firefox在发起同源请求时,不会包含一个Origin头部,但是Chrome和Safari下,除发起同源GET请求不会包含Origin头部外,发起同源POST/PUT/DELETE请求时,都会包含Origin头部。例如,下面就是一个包含Origin头部的同源请求:

POST /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.bob.com

好消息是,对于同源请求,浏览器不会期望服务器返回CORS响应头。因此不管是否有CORS标头,同源请求的响应都是直接发送给用户。然而,如果我们服务器代码返回一个错误,假设源信息Origin不在服务器请求列表中,那么要在头部Origin中包含请求源。

下面是一个关于CORS有效的服务器响应:

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

所有和CORS相关的头部都是以"Access-Control-"开头。更多,见下:

Access-Control-Allow-Origin(必须)-该请求头必须包含在所有合法的CORS响应头中;否则,省略该响应头会导致CORS请求失败。该值要么与请求头Origin的值一样(如上述例子),要么设置成星号‘*’,以匹配任意Origin。如果你想任何站点都能获取到你的数据,那么就使用‘*’吧。但是,如果你想有效的控制,就将该值设置为一个实际的值。

Access-Control-Allow-Credentials(可选)-默认情况下,发送CORS请求,cookies是不会附带发送的。但是,通过使用该响应头就可以让cookies包含在CORS请求中。注意,该响应头只有唯一的合法值true(全部小写)。如果你不需要cookies值,就不要包含该响应头了,而不是将该响应头的值设置成false。该响应头Access-Control-Allow-Credentials需要与XMLHttpRequest2对象的withCredentials属性配合使用。当这两个属性同时设置为true时,cookies才能附带。例如,withCredentials被设置成true,但是响应头中不包含 Access-Control-Allow-Credentials响应头,那么该请求就会失败(反之亦然)。发送CORS请求时,最好不要携带cookies,除非你确定你想在请求中包含cookie。

Access-Control-Expose-Headers(可选)-XMLHttpRequest2对象有一个getResponseHeader()方法,该方法返回一个特殊响应头值。在一个CORS请求中,getResponseHeader()方法仅能获取到简单的响应头,如下:

.Cache-Control

.Content-Language

.Content-Type

.Expires

.Last-Modified

.Pragma

如果你想客服端能够获取到其他的头部信息,你必须设置Access-Control-Expose-Headers响应头。该响应头的值可以为响应头的名称,不过需要利用逗号隔开,这样客服端就能通过getResponseHeader方法获取到了。

-处理一个非简单请求-

在上面,我们一起学习了简单请求GET,但是倘若我们想做更多的事情呢?比如,我们想使用PUT或者DELETE请求,又或者我们想使用Content-Type:application/json来支持JSON。那么,我们就需要掌握该节讲述的‘非简单请求’了。

我们在使用非简单请求时,表面上看起来客户端只发送了一个请求,但实际上,要完成一次非简单请求,客户端在私底下是要向服务器发起两次请求的。第一次请求,是向服务器确认权限,一旦被授权,则发起第二次请求(真正意义上的数据请求)。且,第一次请求也可以被缓存,所以不是每次我们发起非简单请求,都会预请求一次。

例,非简单请求如下:

JavaScript:

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

上面代码中,HTTP请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header。

浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的HTTP头信息。

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

和简单请求一样,浏览器自动将Origin头部信息添加到每个请求中,包括这里的预检查请求。预检查请求用的方法是OPTIONS(所以请确保我们的服务器能够响应该方法)。且,它也包含两个特殊的头部信息,如下:

Access-Control-Request-Method:该字段表示实际的CORS是什么HTTP方法,如上述的PUT方法,且该字段是必须的,即使是简单请求的方法(GET,POST,HEAD)。

Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,如上述的X-Custom-Header。

在上面我们已经提到,预检查请求的目的是向服务器确认实际的 CORS请求权限,那么它是如何检查的呢。

其实,就是验证预检查请求中的两个特殊的请求头(Access-Control-Request-Method和Access-Control-Request-Headers)来裁定的。服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和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-Origin。

Access-Control-Allow-Methods (必须)--它是逗号分隔的一个字符串,值由HTTP方法构成,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。因为已提过预检查请求可以被缓存,所以这样可以避免多次"预检"请求。

Access-Control-Allow-Headers--如果浏览器请求包括Access-Control-Request-Headers字段,则该字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段,因为可以缓存嘛。

Access-Control-Allow-Credentials(可选)—和简单请求一样,详见上述简单请求中的该字段。

Access-Control-Max-Age(可选)--如果每次发起一个非简单的CORS请求,都暗地向服务器发送两次请求,那代价也太大了点,所以该字段可以指定预检查请求可以被缓存多少秒。

一旦预检查得到授权信息,那么浏览器就会发送真正的跨域请求了。且,请求和服务器响应与简单CORS请求一样。

第二次请求(实际CORS请求),如下:

PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.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

如果服务端想要拒绝该CORS请求,那么它可以返回一个普通的响应(如HTTP 200),即不包含任何属于CORS的头部信息。如果预检查请求没有被审核通过,即没有任何关于CORS头部信息的响应,那么浏览器是不会发起第二次实际的请求的,如下服务器响应预检查请求:

//错误-没有CORS头部信息,所以表示是一个无效请求
Content-Type: text/html; charset=utf-8

且会触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息:

原文:https://www.html5rocks.com/en/tutorials/cors/?redirect_from_locale=zh

时间: 2024-10-08 06:49:58

CORS详解[译]的相关文章

CORS详解

介绍 由于同源策略的缘故,以往我们跨域请求,会使用诸如JSON-P(不安全)或者代理(设置代理和维护繁琐)的方式.而跨源资源共享(Cross-Origin Resource Sharing)是一个W3C规范,其建立在XMLHttpRequest对象之上,允许开发人员像使用同源请求一样的规则,在浏览器端发送跨域请求. CORS的使用场景很简单.例如,站点bob.com想要请求获取alice.com的数据,由于同源策略缘故,这种情况在传统请求中是不被允许的.然而,bob.com通过CORS请求ali

跨域资源共享 CORS 详解

一.简介 CORS需要浏览器和服务器同时支持.目前,所有浏览器都支持该功能,IE浏览器不能低于IE10. 整个CORS通信过程,都是浏览器自动完成,不需要用户参与.对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样.浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉. 因此,实现CORS通信的关键是服务器.只要服务器实现了CORS接口,就可以跨源通信. 二.两种请求 浏览器将CORS请求分成两类:简单请求(simple

跨域资源共享CORS详解

简介 CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing). 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制. 本文详细介绍CORS的内部机制. CORS需要浏览器和服务器同时支持.目前,所有浏览器都支持该功能,IE浏览器不能低于IE10. 整个CORS通信过程,都是浏览器自动完成,不需要用户参与.对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样.浏览器

百万年薪python之路 -- 请求跨域和CORS协议详解

楔子 什么是同源策略 同源策略,它是由Netscape提出的一个著名的安全策略.现在所有支持JavaScript 的浏览器都会使用这个策略.所谓同源是指,域名,协议,端口相同.当一个浏览器的两个tab页中分别来自127.0.0.1:8000和127.0.0.1:8001的页面,当浏览器的127.0.0.1:8000的tab页执行一个脚本的时候会检查这个脚本是属于哪个页面的,即检查是否同源,只有和127.0.0.1:8000同源的脚本才会被执行.如果非同源,那么在请求数据时,浏览器会在控制台中报一

【译】UNIVERSAL IMAGE LOADER. PART 3(四个DisplayImage重载方法详解)

在之前的文章,我们重点讲了Android-Universal-Image-Loader的三个主要组件,现在我们终于可以开始使用它了. Android-Universal-Image-Loader有四个重载方法 void displayImage(String url, ImageView view) void displayImage(String url, ImageView view, DisplayImageOptions options) void displayImage(String

【译】UNIVERSAL IMAGE LOADER.PART 2---ImageLoaderConfiguration详解

ImageLoader类中包含了所有操作.他是一个单例,为了获取它的一个单一实例,你需要调用getInstance()方法.在使用ImageLoader来显示图片之前,你需要初始化它的配置-ImageLoaderConfiguration使用init(…)方法.然后,你就可以使用可以明确地根据需要使用不同形式的displayImage(…). 总之,ImageLoader最简单的用法如下所示(使用默认配置): ImageView imageView = ... // view, where th

转:【译】CSS3:clip-path详解

我的一个学生,Heather Banks,想要实现他在Squarespace看到的一个效果: 根据她的以往经验,这个网站的HTML和CSS是完全在她的能力范围以内,于是我帮助她完成了这个效果.显示nav被裁减的效果是一个不简单的任务,我看到图片的第一反应是创建一个相匹配的背景被部分裁剪的图像,然后把它设置为一个after元素.问题是,至少要解决响应式问题,并且响应式并不完全可控的. 了解CSS属性:clip-path clip-path 是工作草案的一部分,它是一个通过屏蔽和裁减来隐藏元素的一部

flowplayer网页视频播放器事例详解--包含各种参数说明(自译)

flowplayer网页视频播放器事例详解--包含各种参数说明(自译) <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server">     <title></title>     <meta http-equiv="content-type" content="text/html; charset=UTF-8&

跨域详解 been blocked by CORS policy: No &#39;Access-Control-Allow-Origin&#39; header is present on the requested resource

跨域问题的原因:浏览器出于安全考虑,限制访问本站点以为的资源. 比如你有一个 网站 127.0.0.1:8080/ , 并且上面挂了一个页面 ,那么在这个页面中 ,你只访问 本站点的 资源不会受到限制,但是你如果访问其他站点,比如  127.0.0.1:8081 的资源就会受到限制. 备注:暂且把  协议,域名,端口都一样的叫做同一个站点. 但是  带有 src 属性的标签可以没有这个 限制,比如 img ,script  等等. 在说说历史,以前的程序前后端不分离, 页面 和 请求接口,在同一