cors

一. 简介

当一个资源从与该资源本身所在的服务器不同的域或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。

而CORS 允许浏览器向跨源服务器,发出跨域请求,从而克服了AJAX只能同源使用的限制。

CORS是一个W3C标准,它同时需要浏览器和服务端的支持,其中浏览器端的支持如下

cors.png

浏览器基本都支持,因此,想要实现CORS通信,只要服务器实现了CORS接口即可。

二. 简单请求与非简单请求

cors请求根据发出的请求是否满足一些条件,将请求分为两种:简单请求与非简单请求。

1. 简单请求

1.1 基本

若请求满足所有下述条件,则该请求可视为“简单请求”:

  • 请求方法为 Head、Get、Post 中的一个
  • 请求头信息中仅包含 对 CORS 安全的首部字段集合 :Accept、Accept-Language、Content-Language、Content-Type,且 Content-Type 仅限于application/x-www-form-urlencoded、multipart/form-data、 text/plain 中的一个。

对于简单请求,浏览器直接发出一个CORS请求,并在请求头中加入一个 Origin 段来描述本次请求来自哪个源(协议 + 域名 + 端口)。如下就是一个简单请求的请求头:

GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Referer: http://foo.example/examples/access-control/simpleXSInvocation.html
Origin: http://foo.example

而服务器则需要在返回头中包含 Access-Control-Allow-Origin 段,来表明哪些外域可以访问。如下是一个返回头

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2.0.61
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml

其中 Access-Control-Allow-Origin:* 表明该资源可以被任意外域访问。如果服务端仅允许来自 http://foo.example 的访问,该首部字段的内容如下:Access-Control-Allow-Origin: http://foo.example。现在,除了 http://foo.example,其它外域均不能访问该资源。

1.2 withCredentials

CORS请求默认不发送Cookie和HTTP认证信息,如果要发送Cookie,首先需要在 AJAX 请求中打开 withCredentials 属性,且服务器的返回头中必须包含 Access-Control-Allow-Credentials:true 。需要注意的是,当请求中包含cookie时,即打开了 withCredentials 后,服务器返回头中 Access-Control-Allow-Origin 就不能再设置为 *,而必须要显示指明允许哪些源访问。

2. 非简单请求

2.1 基本

非简单请求是指那些包含特殊要求的请求,对于这一类请求,浏览器会在正式请求之前发出一个OPTION请求来询问服务器是否支持该次请求,这个OPTION请求被称为‘预检请求’。在服务端回应允许后,浏览器才会发出真正的非简单请求。

2.2 预检请求

如下是一个 预检请求 的请求头:

OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
除了Origin 字段,还包含了Access-Control-Request-Method 和 Access-Control-Request-Headers 字段,分别表明 CORS 请求会用到的 请求方法 和 额外的请求头字段。

服务端在收到预检请求后会做出回应,如下是预检请求的返回头:

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
Access-Control-Allow-Methods 表示服务端允许的请求方法,Access-Control-Allow-Headers 表示服务端允许的请求头额外字段,Access-Control-Max-Age 指定本次预检请求的有效期,有效期内不用发出另一条预检请求。

2.2 实际请求

预检请求完成后,浏览器会将实际请求发出,和简单请求一致。

三. 相关请求、响应头字段

1. 请求

Origin:预检请求或实际请求的源站。协议 + 域名 + 端口。不管是否为跨域请求,ORIGIN 字段总是会被发送。

Access-Control-Request-Method:预检请求专用。将实际请求所使用的 HTTP 方法告诉服务器。

Access-Control-Request-Headers:预检请求专用。将实际请求所携带的额外头字段告诉服务器。

2. 响应

Access-Control-Allow-Origin:指定允许访问该资源的外域 URI。当请求打开 withCredentials 属性时,不可返回 ‘*’。

Access-Control-Expose-Headers:允许浏览器通过 getResponseHeader 访问的额外字段。

Access-Control-Max-Age:预检请求缓存时间。

Access-Control-Allow-Credentials:当请求打开 withCredentials 属性时,需要返回true。

Access-Control-Allow-Methods:预检请求专用。指明实际请求所允许使用的 HTTP 方法。

Access-Control-Allow-Headers:预检请求专用。指明实际请求所允许携带的额外头字段。

四. 常见跨域错误

Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘XXX’ is therefore not allowed access.

No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘XXXX’ is therefore not allowed access.

以上两种错误都是由于服务端返回头没包含Access-Control-Allow-Origin字段导致的,解决办法是添加上该字段并让请求源包含在字段中。


Response to preflight request doesn’t pass access control check: The ‘Access-Control-Allow-Origin’ header contains the invalid value ‘XXX’. Origin ‘XXX’ is therefore not allowed access.

The ‘Access-Control-Allow-Origin’ header contains the invalid value ‘XXX’. Origin ‘XXX’ is therefore not allowed access.

以上两种错误都是由于服务端返回头中的Access-Control-Allow-Origin字段不包含请求源,解决办法是修改该字段使请求源包含在字段中。


Response to preflight request doesn’t pass access control check: The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’. Origin ‘XXX’ is therefore not allowed access. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’. Origin ‘XXX’ is therefore not allowed access. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

以上两种错误都是由于请求打开 withCredentials 属性,但服务端将Access-Control-Allow-Origin设置为了‘’,解决办法是用精确写法替换‘’。


Request header field XXX is not allowed by Access-Control-Allow-Headers in preflight response.

这种错误是由于请求头中包含Access-Control-Request-Headers,但服务器未返回对应的Access-Control-Allow-Headers

什么情况下需要cors?

跨域资源共享标准( cross-origin sharing standard )允许在下列场景中使用跨域 HTTP 请求:

如何实现cors?

跨域请求一直是网页编程中的一个难题,在过去,绝大多数人都倾向于使用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分类

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)。

原文链接:https://liuhuihao.com/http/2018/04/28/HTTP%E8%AE%BF%E9%97%AE%E6%8E%A7%E5%88%B6-CORS-%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0.html

原文链接:http://newhtml.net/using-cors/

posted @ 2019-04-08 22:43  秋宝。  阅读(281)  评论(0编辑  收藏  举报