再谈跨域资源共享 CORS

之前有总结到前端跨域请求,其中一个解决方案是利用CORS,因此有必要详细了解一下CORSCORSCross-origin resource sharing,跨域资源共享 ,是由 W3C 官方推广的允许通过 AJAX 技术跨域获取资源的规范 。

CORS简介

这部分内容摘自:MDN,有幸看到这里的小伙伴建议去详细阅读一下MDN上对CORS的介绍

跨源资源共享 (CORS) (或通俗地译为跨域资源共享)是一种基于HTTP头的机制,该机制通过允许服务器标示除了它自己以外的其它origin(域,协议和端口),这样浏览器可以访问加载这些资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的"预检"请求。在预检中,浏览器发送的头中标示有HTTP方法和真实请求中会用到的头

跨源HTTP请求的一个例子:运行在 http://www.devpoint.cn 的JavaScript代码使用XMLHttpRequest来发起一个到 https://www.devpoint.cn/data.json 的请求。

出于安全性,浏览器限制脚本内发起的跨源HTTP请求。 例如,XMLHttpRequestFetch API遵循同源策略。 这意味着使用这些API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源,除非响应报文包含了正确CORS响应头。

跨源域资源共享( CORS )机制允许 Web 应用服务器进行跨源访问控制,从而使跨源数据传输得以安全进行。现代浏览器支持在 API 容器中(例如 XMLHttpRequest 或 Fetch )使用 CORS,以降低跨源 HTTP 请求所带来的风险。

什么情况下需要 CORS ?

这份 cross-origin sharing standard 允许在下列场景中使用跨站点 HTTP 请求:

怎么使用CORS?

CORS的使用的关键在服务端,浏览器发送请求,服务端接收到客户端请求做一些判断(请求方是否在自己的“白名单”里?),如果没问题就返回数据,否则拒绝。

浏览器将 CORS 请求分成两类:

  • 简单请求(simple request
  • 非简单请求(not-so-simple request

简单请求(simple request

只要同时满足以下两大条件,就是简单请求:

  • 1、请求方法是以下三种方法之一:

    • HEAD
    • GET
    • POST
  • 2、HTTP的头信息不超出以下几种字段:

    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

下面来看看这两种请求,CORS 是怎么处理?

基本流程

对于简单请求,浏览器直接发出CORS请求,在头信息之中,增加一个Origin字段。

下面是来看一个例子,浏览器判断这次跨源AJAX请求是简单请求,就会自动在头信息之中,添加一个Origin字段。

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

接下来服务端收到浏览器请求,首先检测请求报头的 Origin 是否在自己的许可范围内,

如果确实是许可的域,会响应的时候,在响应头额外增加如下字段:

  • Access-Control-Allow-Origin(必选) :这个字段用来告知浏览器,服务端能够接受的发送 AJAX 请求的域,因为此次请求得到许可,所以这里返回与先前请求报头中 Origin 匹配的 http://api.doweb.me。当然,也可以返回 ,表示接受任何域的 AJAX 请求( 是通配的意思)。
  • Access-Control-Allow-Credentials (可选):告知浏览器,是否允许浏览器发送请求的时候携带Cookietrue 表示允许,false 表示禁止,出于安全问题考虑,CORS 默认不允许跨域 AJAX 请求携带 Cookie
  • Access-Control-Expose-Headers(可选):该字段为可选字段。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。

如果不是许可的域,这时候不会返回 Access-Control-Allow-Origin 这个响应头,而浏览器会捕获这次错误,如下图所示

虽然禁止跨域 AJAX 请求携带 Cookie 是为了安全考虑,但由于它在身份验证中的重要性,我们有时候还是得携带 Cookie 的。 具体方法是:

如果需要安全携带Cookie,需要另外一个属性:withCredentials

CORS请求默认不发送CookieHTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。

  • 客户端配置 withCredentials 属性:

    var xhr = new XMLHttpRequest();
    xhr.withCredentials = true;
    
  • 服务端配置 Access-Control-Allow-Credential 为 true,配置 Access-Control-Allow-Origin为指定的域(而不是 * ),

    Access-Control-Allow-Credentials: true
    

如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials

xhr.withCredentials = false;

另外需要注意的是,如果要发送CookieAccess-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie

非简单请求(not-so-simple request

非简单请求包括两次请求,第一次请求是 preflight request,也就是预检/查询请求,这次请求试探性地“询问”服务端,自己打算进行的非简单请求是否合法 —— 不管是否合法,服务端都会通过某种方式通知客户端,客户端基于这个结果,判断是否进行第二次真正的请求。

预检请求

首先是客户端的角度,发送请求时浏览器检测到这是一个非简单请求,所以事先向服务端发送一个预检请求:

OPTIONS /cors HTTP/1.1
Origin: https://www.doweb.me
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Custom-Header1,Custom-Header2
Host: target.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
  • 注意,这里这个预检请求的类型是 OPTIONS
  • 像之前的简单请求一样,这里浏览器会追加一个 Origin,表示请求代码所在的源
  • 前面我们说过,非简单请求会多出额外的请求头字段,这里多出来的就是 Access-Control-Request-MethodAccess-Control-Request-Headers ,这其实是告诉服务端,“我待会要进行的真正请求,类型是这里 Access-Control-Request-Headers 指定的类型,然后自定义请求头是这里 Access-Control-Request-Headers 指定的值,你看看行不行,给我个回应“。
预检请求的回应

服务器收到"预检"请求以后,检查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2020 01:15:39 GMT
Server: Apache/2.0.61(Unix)
Access-Control-Allow-Origin: https://www.doweb.me
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Custom-Header1,Custom-Header2
Access-Control-Max-Age: 1728000
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
  • Access-Control-Allow-Origin:这里和之前一样,可以是 https://www.doweb.me 或者 *,也就是告诉客户端,“我给你的域下了许可证“
  • Access-Control-Allow-Methods:这里告诉客户端,服务端允许的跨域 AJAX 请求的类型,”虽然你刚才告诉我你准备进行的是 PUT 请求,不过你要进行 GET 或者 POST 请求,我也是允许的“
  • Access-Control-Allow-Headers:如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
  • Access-Control-Max-Age: 该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
浏览器的正常请求和回应

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

下面是"预检"请求之后,浏览器的正常CORS请求。

PUT /cors HTTP/1.1
Origin: http://api.doweb.me
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面头信息的Origin字段是浏览器自动添加的。

下面是服务器正常的回应。

Access-Control-Allow-Origin: http://api.doweb.me
Content-Type: text/html; charset=utf-8

上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的。

和JSONP的差异?

CORSJSONP的使用目的相同,但是比JSONP更强大。

JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。