HTTP访问控制(CORS)
Posted bcombettter
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HTTP访问控制(CORS)相关的知识,希望对你有一定的参考价值。
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS
当一个资源请求一个其它域名或者另外一个端口的资源时会产生一个跨域HTTP请求(cross-origin HTTP request)。比如说,http://domaina.example的某html页面通过 <img> 的src 请求 http://domainb.foo/image.jpg。在当今的 Web 开发中,许多页面都会从另外一个站点加载各类资源(包括CSS、图片、javascript 脚本以及其它类资源)。
出于安全考虑,浏览器会限制脚本中发起的跨域请求。比如,使用 XMLHttpRequest
和 Fetch 发起 HTTP 请求就必须遵守同源策略。所以,Web 应用程序通过 XMLHttpRequest
对象或Fetch能且只能向同域名的资源发起 HTTP 请求,而不能向任何其它域名发起请求。 为了改进/提升Web应用程序,开发人员要求浏览器供应商允许跨域请求。 (译者注:这段描述跨域不准确,跨域并非浏览器限制了发起跨站请求,而是跨站请求可以正常发起,但是返回结果被浏览器拦截了。最好的例子是CSRF跨站攻击原理,请求是发送到了后端服务器无论是否跨域!注意:有些浏览器不允许从HTTPS的域跨域访问HTTP,比如Chrome和Firefox,这些浏览器在请求还未发出的时候就会拦截请求,这是一个特例。)
跨源资源共享 ( CORS )机制让Web应用服务器能支持跨站访问控制,从而使得安全地进行跨站数据传输成为可能。需要特别注意的是,这个规范是针对API容器的(比如说 XMLHttpRequest
或者 Fetch ),以减轻跨域HTTP请求的风险。
这篇文章适用于网站管理员、服务器端程序开发人员以及前端开发人员。现代浏览器支持跨源共享的客户端组件,包括请求头和策略执行。同样,服务器端则需要解析这些新的请求头,并按照策略返回相应的响应头以及所请求的资源。对于服务器端程序开发人员,还可以阅读补充材料 cross-origin sharing from a server perspective (with PHP code snippets) 。
跨源资源共享标准( cross-origin sharing standard ) 使得以下场景可以使用跨站 HTTP 请求:
- 如上所述,使用
XMLHttpRequest
或 Fetch发起跨站 HTTP 请求。 - Web 字体 (CSS 中通过
@font-face
使用跨站字体资源), 因此,网站就可以发布 TrueType 字体资源,并只允许已授权网站进行跨站调用。 - WebGL 贴图
- 使用
drawImage
将 Images/video 画面绘制到canvas. - 样式表(使用 CSSOM)
- Scripts (未处理的异常)
接下来的文章,会对跨源资源共享做一个总览,并介绍下需要使用的 HTTP 头。
概述EDIT
跨源资源共享标准通过新增一系列 HTTP 头,让服务器能声明哪些来源可以通过浏览器访问该服务器上的资源。另外,对那些会对服务器数据造成副作用的 HTTP 请求方法(特别是 GET
以外的 HTTP 方法,或者搭配某些MIME类型的 POST
请求),标准强烈要求浏览器必须先以
OPTIONS
请求方式发送一个预请求(preflight request),从而获知服务器端对跨源请求所支持 HTTP 方法。在确认服务器允许该跨源请求的情况下,以实际的 HTTP 请求方法发送那个真正的请求。服务器端也可以通知客户端,是不是需要随同请求一起发送信用信息(包括 Cookies 和 HTTP 认证相关数据)。
随后的章节,将对相关情景及用到的 HTTP 请求进行讨论。
一些访问控制场景EDIT
在此,我们会用三个场景来解释跨源共享是怎么运行的。其中,所有例子使用的都是能发起跨站请求XMLHttpRequest
对象。
如果对以下章节中的 JavaScript 代码片段感兴趣,可以访问 http://arunranga.com/examples/access-control/。在所有支持跨站 XMLHttpRequest
请求的浏览中,可以看到实际运行效果。
而如果想继续了解服务器端对跨源请求的处理,则可以访问Server-Side_Access_Control(CORS)。
简单请求
一些请求不会触发 CORS preflight。而这部分在本文中被称为“简单请求”,虽然 Fetch (定义 CORS的)不使用这个术语。满足下述条件的就是“简单请求”:
- 只允许下列方法:
- 除了用户代理自动设置的头部外(比如
Connection
,User-Agent
,或者其他任意的 Fetch 规范定义的 “禁止的头部名” ),唯一允许人工设置的头部是 Fetch 规范定义的“ CORS-safelisted request-header”,如下:Accept
Accept-Language
Content-Language
Content-Type
(but note the additional requirements below)DPR
Downlink
Save-Data
Viewport-Width
Width
- 允许的
Content-Type
值有:application/x-www-form-urlencoded
multipart/form-data
text/plain
Accept
, Accept-Language
, and Content-Language
头部的值。如果这些头部的值是非标准的,WebKit/Safari 就不会将这些请求视为“简单请求”。WebKit/Safari 并没有将非标准的头部值列个文档出来,但是可以在 WebKit bug 里体现:Require preflight for non-standard CORS-safelisted request headers Accept, Accept-Language, and Content-Language, Allow commas in Accept, Accept-Language, and Content-Language request headers for simple CORS, and Switch to a blacklist model for restricted Accept headers in simple CORS requests。其它的浏览器并没有这个限制的实现,因为这个不在规范的内容里。比如说,假如站点 http://foo.example 的网页应用想要访问 http://bar.other 的资源。以下的 JavaScript 代码应该会在 foo.example 上执行:
var invocation = new XMLHttpRequest();
var url = ‘http://bar.other/resources/public-data/‘;
function callOtherDomain() {
if(invocation) {
invocation.open(‘GET‘, url, true);
invocation.onreadystatechange = handler;
invocation.send();
}
}
这将导致客户端和服务器之间发生简单交换,使用CORS头来处理跨域:
让我们看看,在这个场景中,浏览器会发送什么的请求到服务器,而服务器又会返回什么给浏览器:
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
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
[XML Data]
第 1~10 行是发出的请求头。注意看第10行的请求头 Origin
,它表明了该请求来自于http://foo.exmaple
。
第 13~22 行则是 http://bar.other 服务器的响应。如第16行所示,服务器返回了响应头 Access-Control-Allow-Origin
。使用 Origin 和 Access-Control-Allow-Origin 就能完成最简单的访问控制。本例中,服务器返回了 Access-Control-Allow-Origin: *
,这意味着该资源跨域在跨域行为里可以被任意站点访问。如果服务器端仅允许来自 http://foo.example 的跨站请求,它可以返回:
Access-Control-Allow-Origin: http://foo.example
现在,除了 http://foo.example,其它站点就不能跨站访问 http://bar.other 的资源了(在请求头里 ORIGIN 中定义,见上述第10行)。Access-Control-Allow-Origin
需要为 * 或者包含由 Origin 指明的站点。
预请求
不同于上面讨论的简单请求,“预请求”要求必须先发送一个 OPTIONS
方法请求给目的站点,来查明这个跨站请求对于目的站点是不是安全的可接受的。这样做,是因为跨站请求可能会对目的站点的数据产生影响。 当请求具备以下条件,就会被当成预请求处理:
- 使用下述方法以外的请求:
- 除了用户代理自动设置的头部外(比如
Connection
,User-Agent
,或者其他任意的 Fetch 规范定义的 “禁止的头部名” ),预请求不包括 Fetch 规范定义的“CORS-safelisted request-header”:Accept
Accept-Language
Content-Language
Content-Type
(but note the additional requirements below)DPR
Downlink
Save-Data
Viewport-Width
Width
-
Content-Type
头部的值除了下列之外的:application/x-www-form-urlencoded
multipart/form-data
text/plain
注意: WebKit Nightly 和 Safari Technology Preview 添加了额外的限制在 Accept
, Accept-Language
, and Content-Language
头部的值。如果这些头部的值是非标准的,WebKit/Safari 就不会将这些请求视为“简单请求”。WebKit/Safari 并没有将非标准的头部值列个文档出来,但是可以在 WebKit bug 里体现:Require preflight for non-standard CORS-safelisted request headers Accept, Accept-Language, and Content-Language, Allow commas in Accept, Accept-Language, and Content-Language request headers for simple CORS, and Switch to a blacklist model for restricted Accept headers in simple CORS requests。其它的浏览器并没有这个限制的实现,因为这个不在规范的内容里。
示例如下:
var invocation = new XMLHttpRequest();
var url = ‘http://bar.other/resources/post-here/‘;
var body = ‘<?xml version="1.0"?><person><name>Arun</name></person>‘;
function callOtherDomain(){
if(invocation)
{
invocation.open(‘POST‘, url, true);
invocation.setRequestHeader(‘X-PINGOTHER‘, ‘pingpong‘);
invocation.setRequestHeader(‘Content-Type‘, ‘application/xml‘);
invocation.onreadystatechange = handler;
invocation.send(body);
}
}
......
如上,以 XMLHttpRequest 创建了一个 POST 请求,为该请求添加了一个自定义请求头(X-PINGOTHER: pingpong),并指定数据类型为 application/xml。所以,该请求是一个“预请求”形式的跨站请求。
让我们看看服务器与浏览器之间具体的交互过程:
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
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
Access-Control-Max-Age: 1728000
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
POST /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
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: http://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: http://foo.example
Pragma: no-cache
Cache-Control: no-cache
Arun
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain
[Some GZIP‘d payload]
第1至12行,使用一个 OPTIONS 发送了一个“预请求”。
Firefox 3.1 根据请求参数,决定需要发送一个“预请求”,来探明服务器端是否接受后续真正的请求。 OPTIONS 是 HTTP/1.1 里的方法,用来获取更多服务器端的信息,是一个不应该对服务器数据造成影响的方法。 随同 OPTIONS 请求,以下两个请求头一起被发送:
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER
请求头Access-Control-Request-Method
可以提醒服务器跨站请求将使用POST方法,而
Access-Control-Request-Headers则告知服务器该跨站请求将携带一个自定义请求头请求头
X-PINGOTHER。这样,服务器就可以决定,在当前情况下,是否接受该跨站请求访问。
第15至27行是服务器的响应。该响应表明,服务器接受了客服端的跨站请求。具体可以看看第18至21行:
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER
Access-Control-Max-Age: 1728000
响应头Access-Control-Allow-Methods表明服务器可以接受
POST
, GET和
OPTIONS
的请求方法。请注意,这个响应头类似于HTTP/1.1 Allow: response header,但仅限于访问控制的场景下。而响应头Access-Control-Allow-Headers则表示服务器接受自定义请求头
就像X-PINGOTHER。
Access-Control-Allow-Methods一样,
Access-Control-Allow-Headers允许以逗号分隔,传递一个可接受的自定义请求头列表。最后,
响应头Access-Control-Max-Age告诉浏览器
,本次“预请求”的响应结果有效时间是多久。在上面的例子里,1728000秒代表着20天内,浏览器在处理针对该服务器的跨站请求,都可以无需再发送“预请求”,只需根据本次结果进行判断处理。
附带凭证信息的请求
XMLHttpRequest
和访问控制功能,最有趣的特性就是,发送凭证请求(HTTP Cookies和验证信息)的功能。一般而言,对于跨站请求,浏览器是不会发送凭证信息的。但如果将XMLHttpRequest
的一个特殊标志位设置为true,浏览器就将允许该请求的发送。
http://foo.example站点的脚本向
值。脚本代码如下:http://bar.other站点发送一个GET请求,并设置了一个Cookies
var invocation = new XMLHttpRequest();
var url = ‘http://bar.other/resources/credentialed-content/‘;
function callOtherDomain(){
if(invocation) {
invocation.open(‘GET‘, url, true);
invocation.withCredentials = true;
invocation.onreadystatechange = handler;
invocation.send();
}
如你所见,第七行代码将XMLHttpRequest
的withCredentials标志设置为true,
从而使得Cookies可以随着请求发送。因为这是一个简单的GET请求,所以浏览器不会发送一个“预请求”。但是,如果服务器端的响应中,如果没有返回Access-Control-Allow-Credentials: true的响应头,那么浏览器将不会把响应结果传递给发出请求的脚本程序,以保证信息的安全。
客户端与服务器端交互示例如下:
GET /resources/access-control-with-credentials/ 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/credential.html
Origin: http://foo.example
Cookie: pageAccess=2
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:34:52 GMT
Server: Apache/2.0.61 (Unix) php/4.4.7 mod_ssl/2.0.61 OpenSSL/0.9.7e mod_fastcgi/2.4.2 DAV/2 SVN/1.4.2
X-Powered-By: PHP/5.2.6
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 106
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
[text/plain payload]
虽然第11行指定了要提交到http://bar.other的内容的Cookie信息,但是如果bar.other的响应头里没有Access-Control-Allow-Credentials:true(第19行),则响应会被忽略. 特别注意: 给一个带有withCredentials的请求发送响应的时候,服务器端必须指定允许请求的域名,不能使用‘*‘.上面这个例子中,如果响应头是这样的:Access-Control-Allow-Origin: * ,则响应会失败. 在这个例子里,因为
Access-Control-Allow-Origin的值是http://foo.example这个指定的请求域名,所以服务器端把带有凭证信息的内容返回给了客户端. 另外注意第22行,更多的cookie信息也被创建了.
上面这些例子的运行可以查看这里.下一部分将讨论实际的HTTP头信息.
HTTP响应头EDIT
这部分里列出了跨域资源共享(Cross-Origin Resource Sharing)时,服务器端需要返回的响应头信息.上一部分内容是这部分内容在实际运用中的一个概述.
Access-Control-Allow-Origin
返回的资源需要有一个 Access-Control-Allow-Origin 头信息,语法如下:
Access-Control-Allow-Origin: <origin> | *
origin参数指定一个允许向该服务器提交请求的URI.对于一个不带有credentials的请求,可以指定为‘*‘,表示允许来自所有域的请求.
举个例子,允许来自 http://mozilla.com 的请求,你可以这样指定:
Access-Control-Allow-Origin: http://mozilla.com
如果服务器端指定了域名,而不是‘*‘,那么响应头的Vary值里必须包含Origin.它告诉客户端: 响应是根据请求头里的Origin的值来返回不同的内容的.
Access-Control-Expose-Headers
Requires Gecko 2.0(Firefox 4 / Thunderbird 3.3 / SeaMonkey 2.1)
设置浏览器允许访问的服务器的头信息的白名单:
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
这样, X-My-Custom-Header
和 X-Another-Custom-Header这两个头信息,都可以被浏览器得到.
Access-Control-Max-Age
这个头告诉我们这次预请求的结果的有效期是多久,如下:
Access-Control-Max-Age: <delta-seconds>
delta-seconds
参数表示,允许这个预请求的参数缓存的秒数,在此期间,不用发出另一条预检请求.
Access-Control-Allow-Credentials
告知客户端,当请求的credientials属性是true的时候,响应是否可以被得到.当它作为预请求的响应的一部分时,它用来告知实际的请求是否使用了credentials.注意,简单的GET请求不会预检,所以如果一个请求是为了得到一个带有credentials的资源,而响应里又没有Access-Control-Allow-Credentials头信息,那么说明这个响应被忽略了.
Access-Control-Allow-Credentials: true | false
带有credential的请求在上面讨论.
Access-Control-Allow-Methods
指明资源可以被请求的方式有哪些(一个或者多个). 这个响应头信息在客户端发出预检请求的时候会被返回. 上面有相关的例子.
Access-Control-Allow-Methods: <method>[, <method>]*
发出预检请求的例子见上,这个例子里就有向客户端发送Access-Control-Allow-Methods响应头信息.
Access-Control-Allow-Headers
也是在响应预检请求的时候使用.用来指明在实际的请求中,可以使用哪些自定义HTTP请求头.比如
Access-Control-Allow-Headers: X-PINGOTHER
这样在实际的请求里,请求头信息里就可以有这么一条:
X-PINGOTHER: pingpong
可以有多个自定义HTTP请求头,用逗号分隔.
Access-Control-Allow-Headers: <field-name>[, <field-name>]*
HTTP 请求头EDIT
这部分内容列出来当浏览器发出跨域请求时可能用到的HTTP请求头.注意这些请求头信息都是在请求服务器的时候已经为你设置好的,当开发者使用跨域的XMLHttpRequest的时候,不需要手动的设置这些头信息.
Origin
表明发送请求或者预请求的域
Origin: <origin>
参数origin是一个URI,告诉服务器端,请求来自哪里.它不包含任何路径信息,只是服务器名.
注意,不仅仅是跨域请求,普通请求也会带有ORIGIN头信息.
Access-Control-Request-Method
在发出预检请求时带有这个头信息,告诉服务器在实际请求时会使用的请求方式
Access-Control-Request-Method: <method>
相关的例子可以在这里找到
Access-Control-Request-Headers
在发出预检请求时带有这个头信息,告诉服务器在实际请求时会携带的自定义头信息.如有多个,可以用逗号分开.
Access-Control-Request-Headers: <field-name>[, <field-name>]*
相关的例子可以在这里找到
浏览器支持EDIT
Feature | Chrome | Firefox (Gecko) | Internet Explorer | Opera | Safari |
---|---|---|---|---|---|
Basic support | 4 | 3.5 | 8 (via XDomainRequest) 10 |
12 | 4 |
注意:
Internet Explorer 8 和 9 通过 XDomainRequest 对象来实现CORS ,但是在IE 10中有完整的实现。Firefox 3.5 就引入了对跨站 XMLHttpRequests 和 Web 字体的支持 ,尽管存在着一些直到后续版本才取消的限制。特别的, Firefox 7 引入了对跨站 WebGL 纹理的 HTTP 请求的支持,而且 Firefox 9 添加对通过 drawImage 在 canvas 上绘图的支持。
相关链接EDIT
- Code Samples Showing
XMLHttpRequest
and Cross-Origin Resource Sharing - Cross-Origing Resource Sharing From a Server-Side Perspective (PHP, etc.)
- Cross-Origin Resource Sharing specification
XMLHttpRequest
- Further Discussion of the Origin Header
- Using CORS with All (Modern) Browsers
- Using CORS - HTML5 Rocks
以上是关于HTTP访问控制(CORS)的主要内容,如果未能解决你的问题,请参考以下文章