每一个WEB开发都需要知道的CORS

CORS存在感非常低,但它又存在于几乎所有的WEB页面中。

CORS是什么?

CORS 全称是”跨域资源共享”(Cross-origin resource sharing)。是W3C定义的一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个源 (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求

为什么会有CORS?

CORS的诞生源于浏览器的同源安全策略。所谓的同源,即协议相同域相同端口号相同。基于同源策略,浏览器会对脚本内发起的跨源HTTP请求(如XMLHttpRequest和Fetch API )进行控制,如果不满足同源策略请求会被限制或者拦截返回结果。这意味着使用这些API的Web应用只能请求同域的HTTP资源,除非响应报文包含了正确CORS响应头。

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

什么情况下需要 CORS ?

跨域资源共享标准允许在下列场景中使用跨域 HTTP 请求:

  • 由 XMLHttpRequest 或 Fetch 发起的跨域 HTTP 请求。
  • Web 字体 (CSS 中通过 @font-face 使用跨域字体资源)
  • WebGL 贴图
  • 使用 drawImage 将 Images/video 画面绘制到 canvas
  • 样式表(使用 CSSOM

CORS如何管理来自外部资源的请求?

跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

以下是CORS标准添加的新HTTP标头:

CORS请求访问控制场景示例

接下来通过几个示例来解释跨域资源共享机制的工作原理。

简单请求和非简单请求

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。浏览器对这两种请求的处理,是不一样的。

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

  1. 使用下列方法之一:

    • GET
    • HEAD
    • POST
  2. Fetch 规范定义了对 CORS 安全的首部字段集合,不得人为设置该集合之外的其他首部字段。该集合为:

    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type (值仅限于 text/plain、multipart/form-data、application/x-www-form-urlencoded 三者之一)
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
    • 请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。
    • 请求中没有使用 ReadableStream 对象。

不满足上述条件的视为非简单请求

简单请求

对于简单请求,浏览器只需要通过请求首部的 Origin 和响应首部的 Access-Control-Allow-Origin 就能完成跨域权限的控制。

如下例子:

请求报文和响应报文如下:

  1. // request header
  2. GET /test HTTP/1.1
  3. ...
  4. Referer: https://liayal.com/test.html
  5. Origin: https://liayal.com
  6. ....
  7. // response header
  8. HTTP/1.1 200 OK
  9. ...
  10. Server: nginx/1.12.2
  11. Access-Control-Allow-Origin: *
  12. Content-Type: application/xml
  13. ...

请求首部中的Origin表示当前请求的源,上面的例子则表明该请求来源于 https://liayal.com。

响应首部的Access-Control-Allow-Origin则指定了允许访问该资源的外域URI。对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符,表示允许来自所有域的请求。示例中的中 Access-Control-Allow-Origin 返回为 * 表示允许所有外域访问(一般不会这么配置)。

如果Access-Control-Allow-Origin指定了具体的域名而非*(如:https://liayal.com )那么除了指定的域名外,其它外域均不能访问该资源。Access-Control-Allow-Origin 应当为 * 或者包含由 Origin 首部字段所指明的域名。

如果Origin指定的源,不在许可范围内,服务器会返回一个不包含Access-Control-Allow-Origin字段的响应,浏览器会抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。但是这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

非简单请求

非简单请求再发送实际请求之前会增加一次请求方法为OPTIONS预检请求,以获知服务器是否允许该实际请求。

浏览器通过预检请求获取服务器允许的请求方法和请求头,以及当前域是否在服务器的许可名单之内,只有得到肯定答复,浏览器才会发出正式的Http请求,否则就报错。预检请求的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

查看以下代码:

  1. cosnt xhr = new XMLHttpRequest();
  2. const url = 'https://api.liayal.com/test/';
  3. const body = '{"a": 1}';
  4. xhr.open('POST', url, true);
  5. xhr.setRequestHeader('X-Custom-Header', 'test');
  6. xhr.setRequestHeader('Content-Type', 'application/json');
  7. xhr.send(body);

上面的代码通过POST请求向服务端发送了一个json,请求中包含了一个自定义的请求首部字段(X-Custom-Header: test),同时Content-Type 为 application/json。根据上文的条件,这是一个非简单请求需要发送预检请求。

  1. // request header
  2. OPTIONS /test/ HTTP/1.1
  3. ...
  4. Origin: https://www.liayal.com
  5. Access-Control-Request-Method: POST
  6. Access-Control-Request-Headers: X-Custom-Header, Content-Type
  7. ...
  8. // response header
  9. HTTP/1.1 200 OK
  10. ...
  11. Access-Control-Allow-Origin: https://www.liayal.com/
  12. Access-Control-Allow-Methods: POST, GET, OPTIONS
  13. Access-Control-Allow-Headers: X-Custom-Header, Content-Type
  14. Access-Control-Max-Age: 86400
  15. Vary: Accept-Encoding, Origin
  16. ...

上面预检请求的请求报文中,增加了两个请求首部字段:

Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-Custom-Header, Content-Type

首部字段 Access-Control-Request-Method 告知服务器,实际请求将使用 POST 方法, 而Access-Control-Request-Headers 则告知服务器,实际请求将携带两个自定义请求首部字段:X-Custom-Header 与 Content-Type。服务器据此决定,该实际请求是否被允许。

接下来看预检请求的响应部分,接下来的实际请求能否继续取决于下面几个响应首部:

Access-Control-Allow-Origin: https://www.liayal.com/
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-Custom-Header, Content-Type

首部字段 Access-Control-Allow-Methods 表明服务器允许客户端使用 POST, GET 和 OPTIONS 方法发起请求。

首部字段 Access-Control-Allow-Headers 表明服务器允许请求中携带字段 X-PINGOTHER 与 Content-Type。

首部字段 Access-Control-Max-Age 表明该响应的有效时间为 86400 秒,也就是 24 小时。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。

上述CORS响应首部只有在服务器判定允许跨域请求后返回,如果预检请求不通过,会返回一个正常的HTTP响应,但是没有任何CORS头部信息。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。

  1. XMLHttpRequest cannot load https://api.liayal.com.
  2. Originhttps://www.liayal.com is not allowed by Access-Control-Allow-Origin.

预检请求通过后,接下来就可以发送实际请求了,

  1. // request header
  2. POST /test/ HTTP/1.1
  3. ...
  4. X-Custom-Header: test
  5. Content-Type: application/json; charset=UTF-8
  6. Referer: https://www.liayal.com/test
  7. Origin: https://www.liayal.com
  8. ...
  9. "{"a": 1}"
  10. // response header
  11. HTTP/1.1 200 OK
  12. Access-Control-Allow-Origin: http://foo.example
  13. Vary: Accept-Encoding, Origin
  14. Content-Type: text/plain
  15. ...

上述请求过程中Access-Control-Allow-Origin字段每次响应中必定包含。

附带身份凭证的跨域请求

一般情况下,对于跨域请求,浏览器不会发送身份凭证信息。如果要发送身份凭证信息,需要手动设置相关参数。

如下,我们从 https://www.liayal.com/testhttps://api.liayal.com 发起一个 get 请求。

  1. cosnt xhr = new XMLHttpRequest();
  2. const url = 'https://api.liayal.com/test';
  3. xhr.open('GET', url, true);
  4. xhr.withCredentials = true;
  5. xhr.send();

上面的代码中,我们将 XMLHttpRequest 的 withCredentials 设置为 true,在请求发起时会向服务器发送认证信息(Cookie)。需要注意的是,如果服务器端的响应中未携带 Access-Control-Allow-Credentials: true ,浏览器将不会把响应内容返回给请求的发送者。

请求报文:

  1. // request header
  2. GET /test HTTP/1.1
  3. Referer: https://www.liayal.com/test
  4. Origin: https://www.liayal.com
  5. Cookie: token=jlfkafadsadf
  6. ...
  7. // response header
  8. HTTP/1.1 200 OK
  9. ...
  10. Access-Control-Allow-Origin: https://www.liayal.com
  11. Access-Control-Allow-Credentials: true
  12. Vary: Accept-Encoding, Origin
  13. Content-Type: text/plain
  14. ...

对于附带身份凭证的请求,服务器不得设置 Access-Control-Allow-Origin 的值为*

希望本文能让你对跨域请求有一个清晰的认识,有问题欢迎留言。(完)

写于 2019年02月12日Web Http 11708

如非特别注明,文章皆为原创。

转载请注明出处: https://www.liayal.com/article/5c623937c0ab13505eeefaab

记小栈小程序上线啦~搜索【记小栈】【点击扫码】体验

你不想说点啥么?
😀😃😄😁😆😅😂🤣☺️😊😇🙂🙃😉😌😍😘😗😙😚😋😜😝😛🤑🤗🤓😎🤡🤠😏😒😞😔😟😕🙁☹️😣😖😫😩😤😠😡😶😐😑😯😦😧😮😲😵😳😱😨😰😢😥🤤😭😓😪😴🙄🤔🤥😬🤐🤢🤧😷🤒🤕😈👿👹👺💩👻💀☠️👽👾🤖🎃😺😸😹😻😼😽🙀😿😾👐👐🏻👐🏼👐🏽👐🏾👐🏿🙌🙌🏻🙌🏼🙌🏽🙌🏾🙌🏿👏👏🏻👏🏼👏🏽👏🏾👏🏿🙏🙏🏻🙏🏼🙏🏽🙏🏾🙏🏿🤝👍👍🏻👍🏼👍🏽👍🏾👍🏿👎👎🏻👎🏼👎🏽👎🏾👎🏿👊👊🏻👊🏼👊🏽👊🏾👊🏿✊🏻✊🏼✊🏽✊🏾✊🏿

评论

纵浪02-21 2019

谢谢,正好解决了最近的一些疑惑。

fanerge02-15 2019

不错,跟MDN很同步