问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

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

创作时间:
作者:
@小白创作中心

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

引用
1
来源
1.
https://www.codercto.com/a/60040.html

CORS(跨域资源共享)是Web开发中一个重要的安全机制,它允许浏览器在一定条件下访问不同源的资源。本文将详细介绍CORS的概念、原理和具体应用场景,帮助开发者更好地理解和使用这一技术。

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
  1. 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
  1. 请求中的任意XMLHttpRequestUpload对象均没有注册任何事件监听器;XMLHttpRequestUpload对象可以使用XMLHttpRequest.upload属性访问。
  2. 请求中没有使用ReadableStream对象。

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

简单请求

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

如下例子:

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

// request header
GET /test HTTP/1.1
...
Referer: https://liayal.com/test.html
Origin: https://liayal.com
....
// response header
HTTP/1.1 200 OK
...
Server: nginx/1.12.2 
Access-Control-Allow-Origin: *
Content-Type: application/xml
...

请求首部中的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请求,否则就报错。预检请求的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

查看以下代码:

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

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

// request header
OPTIONS /test/ HTTP/1.1
...
Origin: https://www.liayal.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-Custom-Header, Content-Type
...
// response header
HTTP/1.1 200 OK
...
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-Max-Age: 86400
Vary: Accept-Encoding, Origin
...

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

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回调函数捕获。控制台会打印出如下的报错信息。

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

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

// request header
POST /test/ HTTP/1.1
...
X-Custom-Header: test
Content-Type: application/json; charset=UTF-8
Referer: https://www.liayal.com/test
Origin: https://www.liayal.com
...
"{"a": 1}"
// response header
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://foo.example
Vary: Accept-Encoding, Origin
Content-Type: text/plain
...

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

附带身份凭证的跨域请求

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

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

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

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

请求报文:

// request header
GET /test HTTP/1.1
Referer: https://www.liayal.com/test
Origin: https://www.liayal.com
Cookie: token=jlfkafadsadf
...
// response header
HTTP/1.1 200 OK
...
Access-Control-Allow-Origin: https://www.liayal.com
Access-Control-Allow-Credentials: true
Vary: Accept-Encoding, Origin
Content-Type: text/plain
...

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

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

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号