CORS(跨源资源共享)

555 阅读7分钟

不知你对以下场景是否熟悉?

面试官;是否遇过跨域问题,如何解决的?

求职者:一般由后端配置CORS跨域

面试官:能具体说说如何配置么?

求职者:......不太了解,都是后端配置的

以上场景对于我来说是历历在目,hai,因为我就是那位求职者。作为一个前端对跨域和跨域相关的知识点不熟悉其实是很不应该的。虽说 CORS 一般由服务端同学配置,但是对其熟悉了解还是非常必要的。如果在面试中表现出对其一无所知的样子,无疑让面试官对你的学习态度和技能掌握有所怀疑。

废话有点多,总之,作为一个前端了解了解 CORS 还是非常必要的。痛定思痛的我好好学习了一波 CORS,并做了以下学习总结。

什么是CORS?

跨源资源共享 (CORS) (或通俗地译为跨域资源共享)是一种机制,该机制使用附加的 HTTP 头来告诉浏览器,准许运行在一个源上的Web应用访问位于另一不同源选定的资源。 当一个Web应用发起一个与自身所在源(域,协议和端口)不同的HTTP请求时,它发起的即跨源HTTP请求。-- MDN

用大白话来说,由于同源策略的限制,跨域请求是被浏览器限制的。但是跨域请求又是很常见的一个需求,所以浏览器制定了 CORS 机制。通过设置 HTTP 头来控制是否可以跨域请求,放宽限制。

跨源HTTP请求的一个例子:运行在 domain-a.com 的JavaScript代码使用XMLHttpRequest来发起一个到 domain-b.com/data.json 的请求。

跨源域资源共享( CORS )机制允许 Web 应用服务器进行跨源访问控制,从而使跨源数据传输得以安全进行。

什么时候需要使用CORS?

  • 跨域的 ajax 和 fetch 请求

  • web 字体 (CSS 中通过 @font-face 使用跨源字体资源)(注意css中字体和图片有所不同,字体文件是会被跨域限制的)

  • WebGL 贴图

  • 使用 drawImage 将 Images/video 画面绘制到 canvas

  • a 标签的 download 属性

功能概述

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

简单请求

某些请求不会触发 CORS 预检请求,我们称之为简单请求,一个简单请求需要满足以下所有条件

  • 使用下列方法之一:

    • GET

    • HEAD

    • POST

  • 除了被用户代理自动设置的首部字段(例如 Connection ,User-Agent)和在 Fetch 规范中定义为 禁用首部名称 的其他首部,允许人为设置的字段为 Fetch 规范定义的 对 CORS 安全的首部字段集合。该集合为:

    • Accept

    • Accept-Language

    • Content-Language

    • Content-Type (需要注意额外的限制)

    • DPR

    • Downlink

    • Save-Data

    • Viewport-Width

    • Width

  • Content-Type 的值仅限于下列三者之一:

    • text/plain

    • multipart/form-data

    • application/x-www-form-urlencoded

  • 请求中没有使用 ReadableStream 对象

  • 请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问

示例:一个简单请求

var invocation = new XMLHttpRequest();

var url = 'http://xxx.com/api/getname';

invocation.open('GET', url, true);

invocation.onreadystatechange = (state) => {
    console.log(state)
};

invocation.send(); 

对于简单请求来说,服务端的 CORS 配置很简单

// 仅仅需要设置可接受的域,浏览器就能正常发送跨域请求了
Access-Control-Allow-Origin: http://xxx.com

非简单请求

我们把不符合简单请求条件的请求称之为非简单请求。

示例:一个非简单请求

var invocation = new XMLHttpRequest();

var url = 'http://xxx.com/api/getname';

invocation.open('GET', url, true);
// 设置了非安全首部,所以不符合简单请求
invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
invocation.setRequestHeader('X-FIEld', 'xxx');

invocation.onreadystatechange = (state) => {
    console.log(state)
};

invocation.send(); 

对于非简单请求来说,为了安全,需要先发送预检请求。

预检请求

预检请求不会携带实体数据,并且对于预检请求服务端不能返回重定向

预检请求是个方法为 OPTIONS 的请求,主要用于查询服务端接受的请求方法和首部字段,其请求带有

Access-Control-Request-Method: GET
Access-Control-Request-Headers: X-PINGOTHER, X-FIEld

Access-Control-Request-Method 告诉服务器实际请求将使用 POST 方法,Access-Control-Request-Headers 告诉服务器请求将携带的自定义请求首部字段为 X-PINGOTHER 和 X-FIEld。服务器据此决定,实际请求是否被允许。

对于预检请求,服务器需要设置以下响应首部,表明服务器将接受后续的实际请求

Access-Control-Allow-Origin: http://foo.example
// 可接受的请求方法
Access-Control-Allow-Methods: POST, GET, OPTIONS
// 可接受的自定义请求首部
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type

预检请求具有有效期,对于同一个请求来说,如果预检请求未失效,对于之后的相同请求不用再发起预检请求

实际请求

当预检请求完成后,如果服务端返回的可接受的请求方法,请求域名,及请求首部符合实际请求的话,浏览器将发起实际请求,对浏览器端来说实际请求和正常请求一致。

对于服务端,实际请求也需要设置CORS可接受的域

// 仅仅需要设置可接受的域,浏览器就能正常发送跨域请求了
Access-Control-Allow-Origin: http://xxx.com

附带身份凭证

对于跨域请求(ajax/fetch)来说,浏览器不会自动发送请求,需要主动设置携带 Cookie

var invocation = new XMLHttpRequest();

var url = 'http://xxx.com/api/getname';

invocation.open('GET', url, true);
invocation.withCredentials = true;

invocation.onreadystatechange = (state) => {
    console.log(state)
};

invocation.send(); 

服务端需要设置可接受的域和接受 Cookie

Access-Control-Allow-Origin: http://xxx.com
Access-Control-Allow-Credentials: true

如果是非简单请求,则预检请求的响应头也需要携带 Access-Control-Allow-Credentials 才能正常携带 Cookie 跨域

相关的首部字段

HTTP 响应首部字段

由服务端进行设置(多配置于服务器 Nginx),是实现 CORS 的主要配置

Access-Control-Allow-Origin

服务端指定可接受的外域,可以为具体域名或通配域名。

Access-Control-Allow-Origin: <origin> | *

注意:对于附带身份凭证的请求不可设置为通配符

Access-Control-Expose-Headers

在跨源访问时,XMLHttpRequest对象的getResponseHeader()方法只能拿到一些最基本的响应头,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果要访问其他头,则需要服务器设置本响应头。

Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header

Access-Control-Max-Age

响应预检测请求,设置预检请求的有效期,单位秒,不可设置超过浏览器限定的最大有效时间

Access-Control-Max-Age: <delta-seconds>

Access-Control-Allow-Credentials

响应预检测请求或实际请求,设置允许附带身份凭证(Cookie),默认为 false

Access-Control-Allow-Credentials: true

Access-Control-Allow-Methods

响应预检测请求,指明了实际请求中允许使用的 HTTP 方法。

Access-Control-Allow-Methods: <method>[, <method>]*

Access-Control-Allow-Headers

响应预检测请求,指明了实际请求中允许携带的首部字段。

Access-Control-Allow-Headers: <field-name>[, <field-name>]*

HTTP 请求首部字段

CORS 的请求首部字段由浏览器主动携带,无需开发者手动设置,这也是前端对于 CORS 不熟悉的原因吧。

Origin

Origin 首部字段表明预检请求或实际请求的源站。

Origin: <origin>

注意,在所有访问控制请求(Access control request)中,Origin 首部字段总是被发送。

Access-Control-Request-Method

用于预检请求。其作用是,将实际请求所使用的 HTTP 方法告诉服务器。

Access-Control-Request-Method: <method>

Access-Control-Request-Headers

用于预检请求。其作用是,将实际请求所携带的首部字段告诉服务器。

Access-Control-Request-Headers: <field-name>[, <field-name>]*

参考