Using CORS(译)
原文地址:https://docs.webplatform.org/wiki/tutorials/using_cors
总结
一篇对"Cross Origin Resource Sharing"(CORS)的简介.
简介
APIS是让你能将富文本网页关联在一起的工具,但是将这个方式应用在浏览器上是很困难的,像JSONP(基于安全考虑被限制)和设置代理(设置繁琐并且维护困难)这样的跨域请求是被浏览器限制的。
Cross-Origin-Resource-Sharing(CORS) 是W3C用于浏览器跨域请求的规范,通过配置XmlHttpRequest对象,CORS允许开发者像使用同源请求一样执行跨域请求的操作.
使用CORS是很简单的,想象一下一个站点alex.com中的部分数据bob.com想去访问这些数据,这种类型的请求在传统的请求因为同源策略的限制是不可能的,然而,由于有CORS得支持,alex.com站点的服务器可以设置一些特殊的响应同步让bob.com访问到这些数据.
在这篇文章你将了解到,CORS的实现需要服务器和客户端协作,幸运的是,如果作为一个客户端开发者,实现的具体细节对你来讲是不可见的,下面将介绍客户端如何发起一个跨域请求以及服务器如何配置以支持跨域请求.
1 客户端发起跨域请求
本节将介绍如何使用JavaScript发起一个跨域请求
1.1 创建XmlHttpRequest对象
首先,你需要创建一个合适的请求对象.
function createCORSRequest(method, url) { var xhr = new XMLHttpRequest(); if ("withCredentials" in xhr) { // for others browers // Check if the XMLHttpRequest object has a "withCredentials" property. // "withCredentials" only exists on XMLHTTPRequest2 objects. xhr.open(method, url, true); } else if (typeof XDomainRequest != "undefined") { // for IE // Otherwise, check if XDomainRequest. // XDomainRequest only exists in IE, and is IE's way of making CORS requests. xhr = new XDomainRequest(); xhr.open(method, url); } else { // Otherwise, CORS is not supported by the browser. xhr = null; } return xhr; } var xhr = createCORSRequest('GET', url); if (!xhr) { throw new Error('CORS not supported'); }
1.2 事件处理
原始的XmlHttpRequest对象只有onreadystatechange一个事件,在这个事件中可以处理所有的服务器响应,虽然在XmlHttpRequest2中依然可以使用它,但是却增加了一些新的事件处理,如下列表所示:
在大多数情况下,你至少需要处理onload和onerror这两个事件.
xhr.onload = function() { var responseText = xhr.responseText; console.log(responseText); // process the response. }; xhr.onerror = function() { console.log('There was an error!'); };
浏览器在onerror事件中对错误信息的报告不是很完善,浏览器会报告这个错误,错误却不能被JavaScript访问到,你知道错误发生了,但是不知道更多的信息.
1.3 withCredentials
在客户端标准的CORS请求默认不会发送或者设置任何cookie,为了将cookie作为请求的一部分,你需要将XmlHttpRequest的withCredentials属性设置为true.
xhr.withCredentials = true
在服务器端你必须通过设置"Access-Control-Allow-Credentials"头部为true才能启用Credentials功能.
Access-Control-Allow-Credentials: true;
在一个请求中withCredentials属性可以包含来自远端域名的任何cookie,并且还可以设置这些cookie,值得注意的是这些cookie任然遵循同源策略,因此你的javascript代码不可以通过domian.cookie或者请求的头部去访问这些cooike的,它们只能被远端域名服务器操作.
1.4 发起请求
现在你的客户端已经都配置好了,通过调用send()函数发送请求.
xhr.send(null);
如果请求中要包含数据体,可以作为send()函数的参数进行传递.
假设你的服务端已经做好了响应CORS请求的准备,客户端的onload事件将与同源请求的XmlHttpRequest一样了.
1.5 完整例子
这是一个完整的客户端CORS请求例子
// Create the XHR object. function createCORSRequest(method, url) { var xhr = new XMLHttpRequest(); if ("withCredentials" in xhr) { // XHR for Chrome/Safari/Firefox. xhr.open(method, url, true); } else if (typeof XDomainRequest != "undefined") { // XDomainRequest for IE. xhr = new XDomainRequest(); xhr.open(method, url); } else { // CORS not supported. xhr = null; } return xhr; } // Helper method to parse the title tag from the response. function getTitle(text) { return text.match('<title>(.*)?</title>')[1]; } // Make the actual CORS request. function makeCorsRequest() { // bibliographica.org supports CORS. var url = 'http://bibliographica.org/'; var xhr = createCORSRequest('GET', url); if (!xhr) { alert('CORS not supported'); return; } // Response handlers. 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(); }
2 服务端配置支持CORS请求
大部分繁重的工作被浏览器和服务器处理了.在CORS工作期间,浏览器会自动补充一些头部信息,有时候甚至会自动发起一些必要的请求,这些对于客户端开发者来讲是不可见的,浏览器帮你做了这些工作(当然可以通过wireshark获得这些细节).
浏览器厂商负责实现这些细节,下面将描述如何配置服务器端头部以支持CORS的请求.
2.1 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.比如JSONP用于跨域请求使用的GET方式.
任何不符合上面标准的请求就是"非简单请求",这个请求在客户端和浏览器之间做了一些额外的通信(叫做"preflight request"),下面详细讲解.
2.2 处理一个简单请求
从客户端的一个"简单请求"开始,下面的JavaScript代码是发送一个跨域"简单请求"的代码,以及浏览器实际发送的"简单请求"的头部信息.
JavaScript:
var url = 'http://api.alice.com/cors'; var xhr = createCORSRequest('GET', url); xhr.send();
HTTP Request:
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头部是浏览器自动添加的,而且不能被JavaScript控制,它的值一般是协议名称(比如http),域名(比如api.bob.com),端口(没有特别说明默认为80)的组合,如 Origin: http://api.bob.com
下面是一个合法的服务端的响应结构:
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请求失败,它的值可以是请求报文中的origin的值(表示此站点的跨域请求被允许)或者是一个符号"*"(表示允许任何站点的跨域请求).
Access-Control-Allow-Credentials(可选项):默认情况下,cookie是不包含在CORS的请求报文中的.使用这个头部就表示cookie是应该包含在CORS的请求报文中的,这个头部的合法值是true(小写),服务器不需要cookie就没必要返回这个头部(或者返回时它的值为false),这个头部的工作和客户端的.withCredentials属性值联系在一起的,两个的值都为true CORS请求才会成功。值得注意的是,除非你确定cookie包含在请求报文中,否则不要设置这两个量.
Access-Control-Expose-Headers(可选项):XmlHttpRequest2对象有一个getResponseHeader()方法可以返回详细的HTTP头部信息,在CORS请求中,getResponseHeader()方法只能获得简单的响应头部信息,简单响应头如下:
- Cache-Control
- Content-Language
- Content-Type
- Expires
- Last-Modified
- Pragma
如果你想在客户端访问除上以外的其他头部详细,你就需要使用Access-Control-Expose-Headers头部,这个头部的值是一个逗号分隔的列表,这个列表会暴露给客户端.
顺便说一下,大多数的浏览器实现的getResponseHeader()方法都是有bug的,会导致即使服务器设置了Access-Control-Expose-Headers头部客户端的getResponseHeader()函数也有可能会访问不到.
2.3 处理一个非简单请求
讲了处理简单的GET请求,但是如果你想做更多的事情该怎么做呢?也许你想是否能支持其他类型的HTTP请求,比如PUT or DELETE,或者是你想使用Content-Type:application/json头来支持请求json数据,这就是所谓的处理非简单的请求.
非简单请求表面上看起来是一个客户端向服务器的单一请求,但是实际上是包含了两个请求.浏览器首先向服务器发送一个预先请求(prelight request),目的是询问服务器我是否有发送实际请求的权限,一旦服务器回应了准许,浏览器才会发出实际请求.浏览器透明地处理这两个请求,预先请求(prelight request)会被缓存下来,因此没有必要每次请求前都发送预先请求.
下面是一个非简单请求的实例:
JavaScript:
var url = 'http://api.alice.com/cors'; var xhr = createCORSRequest('PUT', url); xhr.setRequestHeader( 'X-Custom-Header', 'value'); xhr.send();
预先请求发送的报文头部:
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头部,预先请求使用的是HTTP 的OPTIONS请求方式(要确保服务器可以响应这个请求方式),下面还有一些额外的请求头部:
Access-Control-Request-Method:实际的请求方法,这个请求头部始终是包含了的,就算是一些简单请求的请求方法(像POST,GET,HEAD).
Access-Control-Request-Headers:包含在请求头部中的用逗号分隔的一个列表.
预先请求的作用是为实际请求发出之前询问权限的,服务器应该检验上面的两个头部和在请求头部中的HTTP方法是否支持和接受.如果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...
预先请求的响应报文:
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(必须的):和简单请求的一样,必须包含在响应头部中,如果没有CORS就会失败.
Access-Control-Allow-Methods(必须的):用逗号隔开的服务器支持的HTTP方法的列表,值得注意的是,虽然预先请求只是为单个HTTP方法(例子为PUT方法)询问是否有权限,但是服务器还是会将服务器所有支持的HTTP方法(支持GET,POST,PUT)以列表的方式返回.这是很有帮助的,因为预先请求会被缓存,所以一个单一方法的预先请求的返回信息就包含了其它HTTP方法的支持与否.
Access-Control-Allow-Headers(如果请求报文头部包含了Access-Control-Request-Headers就是必须要存在的):值为用逗号分隔的所有支持的请求头列表,和Access-Control-Allow-Methods一样,它包含了服务器支持的所有头部.
Access-Control-Allow-withCredentials:和上面简单请求的一样.
Access-Control-Max-Age(可选的):在每个实际请求之前都发起一个预先请求是很昂贵的,因为这样每个客户端的请求将会请求两次,所以这个头部就告诉浏览器,在第一次发起预先请求之后的多少秒之内的请求是不需要发送预先请求来询问权限的.
如果服务器想终止CORS的请求,这时候可以返回一个普通的响应(HTTP 200),但是却没有任何CORS头部(Access-Control-开头的)信息,如下:
预先请求:
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...
预先请求响应:
// ERROR - No CORS headers, this is an invalid request! Content-Type: text/html; charset=utf-8
浏览器会打出下面的log:
XMLHttpRequest cannot load http://api.alice.com. Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
预先请求一旦获得了响应,接下来就会发起实际请求.
实际请求:
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