Cross-Origin Resource Sharing(跨域资源共享)

之前学的CORS已经忘的差不多了,这里再总结一下,加深印象。
关于同源策略写的比较好的文章再谈同源策略

什么是CORS

CORS(Cross-Origin Resource Sharing, 跨源资源共享)是W3C出的一个标准,其思想是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。

  • Access-Control-Allow-Origin: 允许跨域访问的域,可以是一个域的列表,也可以是通配符"*"。
    注意Origin规则只对域名有效,并不会对子目录有效。不同子域名需要分开设置。
  • Access-Control-Allow-Credentials: 是否允许请求带有验证信息,这部分将会在下面详细解释
  • Access-Control-Expose-Headers: 允许脚本访问的返回头,请求成功后,脚本可以在XMLHttpRequest中访问这些头的信息(貌似webkit没有实现这个)
  • Access-Control-Max-Age: 缓存此次请求的秒数。在这个时间范围内,所有同类型的请求都将不再发送预检请求而是直接使用此次返回的头作为判断依据,非常有用,大幅优化请求次数
  • Access-Control-Allow-Methods: 允许使用的请求方法,以逗号隔开
  • Access-Control-Allow-Headers: 允许自定义的头部,以逗号隔开,大小写不敏感

为什么要用CORS

  • 使用它的根本原因就是要完成资源的跨域访问,也就是如何绕过Same-origin Policy(在一个浏览器中访问的网站不能访问另一个网站中的数据,除非这两个网站具有相同的Origin,也即是拥有相同的协议、主机地址以及端口。一旦这三项数据中有一项不同,那么该资源就将被认为是从不同的Origin得来的,进而不被允许访问。)。
  • 一个大型网站常常拥有一系列子域。在这些域之间交换数据就会受到Same-origin Policy的限制。为了绕过该限制,业界提出了一系列解决该问题的方法,例如更改document.domain属性,跨文档消息,JSONP以及CORS等。这些解决方案各有各的长处,因此我们需要根据需求的不同来对这些方案进行选择。

CORS运行流程

假设ambergarden.com想从一个公有数据平台public-data.com中返回一些数据,那么在页面逻辑中,其可以通过下面的代码向public-data.com发送数据请求:

 function retrieveData() {
     var request = new XMLHttpRequest();
     request.open('GET', 'http://public-data.com/someData', true);
     request.onreadystatechange = handler;
     request.send();
 }

在运行这段代码的之后,浏览器会向服务发送如下的请求:

GET /someData/ HTTP/1.1
Host: public-data.com
 ......
Referer: http://ambergarden.com/somePage.html
Origin: http://ambergarden.com

而一个支持CORS协议的服务可能会给出下面的响应:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://ambergarden.com
Content-Type: application/xml
.....

[Payload Here]
  • Access-Control-Allow-Origin表示允许跨域的域名,可以设置为*,也可以设置为具体的域,其中,表示全部,即所有的域名下的请求都允许,但设置为*后,所有的请求都不会携带附带身份凭证(比如cookie);设置为具体的域则表示只有该域下的请求允许,别的域下的请求不被允许,设置为具体的域是请求中携带身份凭证的基础。
  • 在接收到服务端响应后,浏览器将会查看响应中是否包含Access-Control-Allow-Origin响应头。如果该响应头存在,那么浏览器会分析该响应头中所标示的内容。如果其包含了当前页面所在的域,那么浏览器就将知道这是一个被允许的跨域访问,从而不再根据Same-origin Policy来限制用户对该数据的访问。

简单请求和非简单请求

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
* 简单请求就是使用设定的请求方式请求数据
* 而非简单请求则是在使用设定的请求方式请求数据之前,先发送一个OPTIONS请求,看服务端是否允许客户端发送非简单请求。只有"预检"通过后才会再发送一次请求用于数据传输

简单请求

* 请求方式:HEAD,GET,POST
* 请求头信息:
    Accept
    Accept-Language
    Content-Language
    Last-Event-ID
    Content-Type 对应的值是以下三个中的任意一个
                        application/x-www-form-urlencoded
                        multipart/form-data
                        text/plain

只有同时满足以上两个条件时,才是简单请求,否则为非简单请求
图片.png

预检请求(preflight request)

不满足简单请求条件的请求则要先进行预检请求,即使用OPTIONS方法发起一个预检请求到服务器,已获知服务器是否允许该实际请求。

预检请求过程
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错

var url = 'http://api.alice.com/cors'; 
var xhr = new XMLHttpRequest(); 
xhr.open('PUT', url, true); 
xhr.setRequestHeader('X-Custom-Header', 'value'); 

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

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

OPTIONS /cors HTTP/1.1 // 预检请求的请求方法是 OPTIONS,表示该请求是用来询问的 
Origin: http://api.bob.com // Origin字段: 表示请求来自哪个源 
Access-Control-Request-Method: PUT // 该字段,表示HTTP请求的方法,如PUT DELETE POST GET 增删改查 
Access-Control-Request-Headers: X-Custom-Header // 指定浏览器CORS请求会额外发送的头信息字段 
Host: api.alice.com 
Accept-Language: en-US 
Connection: keep-alive 
User-Agent: Mozilla/5.0...

预检请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。

除了Origin字段,预检请求的头信息包括两个特殊字段:
* Access-Control-Request-Method ------------------- 必须
该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。
* Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。

预检请求的响应
服务器收到预检请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

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://api.com // 允许该域名请求数据 ,如为星号,表示同意任意跨源请求。 
Access-Control-Allow-Methods: GET, POST, PUT // 必须 
Access-Control-Allow-Headers: X-Custom-Header // 如果请求时有该字段,则是必须 
Content-Type: text/html; charset=utf-8 
Content-Encoding: gzip 
Content-Length: 0 Keep-Alive: timeout=2, max=100 
Connection: Keep-Alive 
Content-Type: text/plain

如果服务器否定了预检请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。

一旦服务器通过了预检请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段

携带身份凭证

大部分的请求是需要用户携带着用户信息的,比如在一个登录的系统中,用户会携带着相应的cookie或token,但CORS跨域默认是不带身份凭证的。

如果需要附带身份凭证,在发送请求时,通过将withCredentials属性设置为true,可以指定某个请求可以发送凭据。

function retrieveData() {
    var request = new XMLHttpRequest();
    request.open('GET', 'http://public-data.com/someData', true);
    request.withCredentials = true;
    request.onreadystatechange = handler;
    request.send();
}

  • 服务端的Access-Control-Allow-Origin头部不能设置为*
  • 服务端的Access-Control-Allow-Credentials头部设置为true

<?php
response.setHeader('Access-Control-Allow-Credentials: true');
response.setHeader('Access-Control-Allow-Origin: http://ambergarden.com');
....

而在服务端的响应中,其将拥有一个额外的Access-Control-Allow-Credentials响应头:

HTTP/1.1 200 OK
Access-Control-Allow-Credentials: http://ambergarden.com
Content-Type: application/xml
 ......

[Payload Here]

参考

再谈同源策略
CORS简介
CORS详解
CORS

CORS配置安全漏洞报告及最佳部署实践

一篇关于CORS一些配置错误导致安全漏洞的文章
绕过浏览器SOP,跨站窃取信息:CORS配置安全漏洞报告及最佳部署实践