通过fetch看跨域:是谁阻止了跨域请求?

21,011 阅读6分钟

1.前言

fetch是一个在js中非常好用的网络请求方法,可以当作一种浏览器原生支持的,xhr的简写升级版。使用时其mode参数可以进行对网络请求跨域时的设置,在研究fetch使用时,想到我们开发时经常会遇到跨域请求失败的情况:

image.png

以前只知道jsonpcors等手段可以解决跨域问题,八股背了一大堆,其本质却不甚明了。所以今天想通过一些实验弄清楚:这个跨域失败究竟是如何失败?在进行到哪一步时失败的?

2.fetch基础

简单来说,fetch是一个网络请求的函数,在浏览器下是个全局函数,可以在console中直接用。其最大好处有两点:

  1. 写起来简单
  2. 返回结果时promise对象 比如我们在掘金首页console中运行如下命令,可以获得首页的html内容:
fetch("https://juejin.cn/")
  .then((response) => response.text())
  .then((data) => {
    console.log(data);
  });

若是使用传统的xhr形式的ajax写法:

const xhr = new XMLHttpRequest(),
    method = "GET",
    url = "https://juejin.cn/";

xhr.open(method, url, true);
xhr.onreadystatechange = function () {
  if(xhr.readyState === XMLHttpRequest.DONE) {
    var status = xhr.status;
    if (status === 0 || (status >= 200 && status < 400)) {
      console.log(xhr.responseText);
    }
  }
};
xhr.send();

况且这种回调式的写法还是不如promise来得爽利,此外fetch还处理流数据的结果,扩展了xhr的使用范围。

2.1 fetch参数

只放第一个参数url时fetch默认用GET方法,还可以配置器第二个参数对象:

fetch(url, optionObj)

具体第二个参数对象内容见下,星号为默认设置:

fetch(url, {
    method: 'POST', // *GET, POST, PUT, DELETE, etc.
    mode: 'cors', // no-cors, *cors, same-origin
    cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
    credentials: 'same-origin', // include, *same-origin, omit
    headers: {
      'Content-Type': 'application/json'
      // 'Content-Type': 'application/x-www-form-urlencoded',
    },
    redirect: 'follow', // manual, *follow, error
    referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
    body: JSON.stringify(data) // body data type must match "Content-Type" header
  });

通过第二个参数的设置,我们可以进行POST请求,设置请求头,请求体等等。

2.2 fetch结果

fetch函数的返回结果是一个promise对象,其中resolve出来的是一个Response对象,通过这个对象我们可以使用一些方法读取其中的数据,并返回一个promise:

response.text() //读文本
response.json() //读json
response.blob() // Blob 对象
response.formData() // FormData 表单对象。
response.arrayBuffer() // 得到二进制 ArrayBuffer 对象

也可以通过一些属性判断请求是否成功,方便进行后续行为

Response.ok //请求是否成功
Response.status //表示 HTTP 回应的状态码
Response.statusText //一个字符串,表示 HTTP 回应的状态信息

3.模拟跨域实验与fetch跨域的mode参数

3.1 模拟跨域

所谓跨域请求,就是浏览器页面与其所请求的页面不同源,即两个地址有协议、域名、端口号三者任一不同则均属跨域请求。 下面模拟一个跨域请求: 现在本地起一个简单的node服务器,以便我们控制服务端行为,代码如下:

var http = require("http");

var data = {
  name: "link",
  age: 121,
};
http
  .createServer(function (request, response) {
    // 不加Access-Control-Allow-Origin响应头就无法实现跨域,*表示所有域都可以请求
    // response.setHeader("Access-Control-Allow-Origin", "*");

    console.log(request.url);
    response.write(JSON.stringify(data));
    response.end();
  })
  .listen(8030, function () {
    console.log("Server has stared...");
  });

然后我们打开掘金首页并打开console,在其中运行网络请求指令就是跨域了,请求一下本地服务器地址:

fetch('http://127.0.0.1:8030/')

因为我们没有在服务端配置跨域,所以熟悉的跨域报错出现了: image.png 看下网络日志,确实是跨域错误: image.png 至此,我们构造了一个跨域情景模拟,在此环境中,我们研究fetch的mode参数设置的不同。

3.2 不同fetch mode参数下的跨域结果

mode参数设置了网络请求的跨域形式:

mode: 'cors', // no-cors, *cors, same-origin

下面分别研究三个参数下跨域情况:

3.2.1 same-origin

在掘金首页执行以下代码:

fetch('http://127.0.0.1:8030/', {mode: 'same-origin'})

该参数下执行严格的同源政策,非同源不发送请求:

image.png

也就是该请求在发送时就被阻止了,服务器端并未收到该请求。示意图如下

image.png

3.2.2 no-cors

no-cors很容易让人误解为不跨域也能请求,类似jsonp这种,但其实不然,no-cors是一种特殊的跨域请求模式。这种模式下服务端没有处理跨域的能力,但是我们仍发起跨域,不过这个请求类似于表单提交,我们并不打算得到什么结果,跨域了也不会报错,回来的响应是可以被忽略的。也就是说,请求是单向的,就是想发点东西到服务器端

fetch('http://127.0.0.1:8030/', {mode: 'no-cors'})

没有报错 image.png 网络请求也正常 image.png 不过其返回的response对象的type为opaque,返回也是无内容的,也就是说并不能得到数据。不过服务器端是可以得到数据的:

fetch('http://127.0.0.1:8030/?name=link', {mode: 'no-cors'})

image.png

示意图如下:

image.png

3.2.3 cors

这个参数是默认参数,前文也可以,看到我们就是用这个命令来模拟最初的跨域失败情景的。具体内容与3.1类似,到但是我们加上一些参数再请求,看看服务端是否接收到数据:

fetch('http://127.0.0.1:8030/?name=link')

跨域失败: image.png

服务端可以接收到数据:

image.png

过程示意图如下:

image.png 若我们在服务器端开启跨域配置,即

response.setHeader("Access-Control-Allow-Origin", "*")

把前文中的注释放出来即可。 那么跨域请求将会和普通同源请求一样顺畅,不需要我们在前端多做什么事情。 image.png

3.3 谁阻止了跨域?

在上述例子中,服务器端并未对发起请求的页面地址作任何分析,所以是否跨域,其实全都是浏览器自己来进行判断的。其过程大致如下:

  1. 判断是否跨域,跨域则在请求头加origin属性。
  2. 服务器返回数据。
  3. 浏览器判断服务器返回数据响应头中有无Access-Control-Allow-Origin,无或不匹配则为跨域。熟悉的报错就会如约而至。匹配成功则无事发生。 不过此处仅是简单请求,复杂请求时需要预检请求option,但其核心还是在服务端的配置。

4.倘若浏览器不禁止跨域

到这儿,又有了个新的疑问。浏览器是不是吃饱了撑的,整这么多跨域来恶心🤢我们开发者啊?这跨域能不能不要?

4.1 实验

别说,还真能不要跨域,但是得对浏览器入口快捷方式进行一定的改造。 改造完了之后:

image.png 运行跨域请求也是畅通无阻:

image.png

4.2 分析

那我们能不能教谷歌做事,让他把跨域限制取消呢?😁

答案是否定的,若没有同源限制,浏览器则是不安全的浏览器,用户怎么会敢使用随使暴雷的浏览器呢?至于其隐患的具体体现则是我们老生常谈的csrf问题。具体内容已超出本文范畴,详见超链接。

5.总结

  • 跨域是浏览器做出的限制,为保证安全牺牲了一定自由度。
  • cors的目的是为了将跨域逻辑后移,让服务器决定谁能跨域。(不过现实是很多后端懒得配代码,前端还是得用jsonp)
  • fetch简化了操作,使用起来比较顺滑

参考

  1. 使用 Fetch
  2. Fetch API 教程
  3. CORS解决跨域请求问题
  4. 谷歌浏览器禁止同源策略支持跨域 | windows, Chrome 80+