跨源资源共享 CORS 小记

423 阅读4分钟

简介

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

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

注意,由于是浏览器判断响应头,因此,服务器实际上已经收到了请求。只不过返回值被浏览器拦截了

实现方式

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

CORS请求失败会产生错误,但是为了安全,在JavaScript代码层面是无法获知到底具体是哪里出了问题。你只能查看浏览器的控制台以得知具体是哪里出现了错误。

触发条件

简单请求并不会触发 CORS,当同时满足以下三个条件时,即为简单请求

某些请求不会触发 CORS 预检请求。本文称这样的请求为“简单请求”,请注意,该术语并不属于 Fetch (其中定义了 CORS)规范。若请求满足所有下述条件,则该请求可视为“简单请求”:

  1. 使用下列方法之一: GET HEAD POST
  2. 除了被用户代理自动设置的首部字段(例如 Connection ,User-Agent)和在 Fetch 规范中定义为 禁用首部名称 的其他首部,允许人为设置的字段为 Fetch 规范定义的 对 CORS 安全的首部字段集合。该集合为: Accept Accept-Language Content-Language Content-Type (需要注意额外的限制) DPR Downlink Save-Data Viewport-Width Width
  3. Content-Type 的值仅限于下列三者之一: text/plain multipart/form-data application/x-www-form-urlencoded

解决方法

  • 服务器使用 Access-Control-Request-Headers 可以用来处理情况二中使用自定义 Header 的情况
  • Access-Control-Allow-Methods 可以用来处理情况一
  • Access-Control-Allow-Origin 用来处理域名白名单
  • Access-Control-Max-Age 表明发起预检请求的缓存时间(注意浏览器本身维护了一个最大值,如果超过,则无效)
  • Access-Control-Expose-Headers 用于要求浏览器携带某些 Header(否则只能拿到最基本的一些)

cookie

一般而言,跨域请求不会携带 cookie。如果需要携带 cookie,则需要在发出请求时进行特殊的设置,首先是在请求时

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/', true); 
xhr.withCredentials = true; 
xhr.send(null);

// Fetch
fetch(url, {
  credentials: 'include'  
})


  1. 请求时需要进行设置
  2. response header 中的 Access-Control-Allow-Credentials: true (如果为 false,则浏览器不会把相应内容返回给发送者)
  3. 服务器不得设置 Access-Control-Allow-Origin 的值为 *。否则请求会失败

另外,响应首部中也携带了 Set-Cookie 字段,尝试对 Cookie 进行修改。如果操作失败,将会抛出异常

CORS 中间件处理

  1. 对于非OPTIONS请求的处理,要根据情况加上 Access-Control-Allow-Origin,Access-Control-Allow-Credentials,Access-Control-Expose-Headers这三个响应头部;
  2. 对于OPTIONS请求(预检请求)的处理,要根据情况加上 Access-Control-Allow-Origin,Access-Control-Allow-Credentials,Access-Control-Max-Age,Access-Control-Allow-Methods,Access-Control-Allow-Headers这几个响应头部;

验证结果

跨域 GET 请求

fetch('http://127.0.0.1:4321').then(function (response) {
  if (response.ok) {
    response.json().then(function (data) {
      console.log(data);
    });
  } else {
    console.log('请求失败,状态码为', response.status);
  }
}, function(err) {
  console.log('出错:', err);
});

跨域简单 POST 请求

fetch('http://127.0.0.1:4321/post', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  body: 'firstName=Nikhil&favColor=blue&password=easytoguess'
}).then(function(res) {
  if (res.ok) {
    console.log('Perfect! Your settings are saved.');
  } else if (res.status == 401) {
    console.log('Oops! You are not authorized.');
  }
}, function(e) {
  console.log('Error submitting form!');
});

失败

跨域不简单 POST 请求(JSON)

fetch('http://127.0.0.1:4321/post', {
  method: 'post',
  headers: {
    'Accept': 'application/json, text/plain, */*',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({a: 7, str: 'Some string: &=&'})
}).then(res=>res.json())
  .then(res => console.log(res));

结果:服务器连请求都收不到

参考资料