跨域-自在极意功版

439 阅读10分钟

该是重新点亮星星的时候了✨✨

前言

感觉跨域是一个老生常谈的问题了。想象一下下面的场景,相信大家都经历过。
// 场景1
我这个请求明明和后端商量的一样啊,为什么会报CORS的错误啊?
// 场景2
学弟/学妹:学长学长,我这请求报错了,是跨域的错误,要让后端搞一下吗?
经验丰富的老学长: 不用,你直接前端把跨域问题解决了就行
学弟: ???
// 场景3
前端: 请求跨域了,你要不搞一下?
后端: 为什么,我听说都是前端在解决?

总有一款是适合你的。
那如何能从头到尾的将跨域理解透彻呢?
首先明确一个逻辑

你违反了同源策略,导致了跨域,浏览器根据CORS跨域资源共享机制发送cors请求,服务端不支持,报CORS error错误。

什么是跨域

之前我的一篇文章中,也同样讲到了这个问题,本文是基于上一篇文章的进化版。

网络安全工程师为了网络安全,为浏览器加上了一个名为同源策略的机制

当我们在进行请求的时候,如果违反了同源策略那么就会导致我们发生跨域。

同源策略

同源策略是一个重要的安全策略,它用于限制一个源domain的文档或者它加载的脚本如何能与另一个源的资源进行交互。

同源策略是浏览器安全的基石。

同源策略的目的,是为了保证用户信息安全,防止恶意的网站窃取数据。最初的含义是指:A网页设置的Cookie,B网页不能打开,除非这两个网页“同源”。

这里的“同源”,指的是

  • 协议相同
  • 域名相同
  • 端口相同

举个例子

当我们在下面的http://www.abc.com:8080的网页下,去请求一个http://www.abc.com:8081/login的接口,那么由于端口号不同,那么就会导致跨域。同理,协议不同,子域名不同,主域名不同同样会导致跨域。

那么,我们应该如何解决跨域问题呢?

跨域问题常用的解决方案

CORS(跨域资源共享)策略

同源策略的提出,对于保护用户隐私和安全非常重要,但它也同时限制了不同域名之间的数据交换和资源共享。

同源策略提升了Web前端的安全性,但是牺牲了Web拓展上的灵活性。设想若把html,js, css, image 等文件全部部署在一台服务器上,或者全部接口都部署在一个服务器。那么服务器会先一步承受不住。
所以,在2004年,微软工程师Bert Bos和Google工程师Ian Hickson提出了CORS(跨域资源共享)。在遵循同源策略的基础上,选择性地为同源策略“开放了后门”。
知乎上一个好的回答
www.zhihu.com/question/25…
CORS解决方案,需要浏览器和服务器同时进行支持。整个CORS通信过程,都是浏览器自动完成的,不需要用户参与。对于开发者来说,CORS通信与同源的通信请求没有什么差异。浏览器一旦发现跨域请求,就会自动的添加一些附加的头信息,有时还会多出一次附加请求(options预检请求),但用户不会有感觉。
那什么时候会有额外的请求,什么时候不会有呢?
对于CORS通信请求,浏览器分为两类

  • 简单请求
  • 非简单请求
简单请求

比较官方的说法 ==>

只要同时满足以下两大条件,就属于简单请求

  1. 请求方法是以下三种方法之一

    • GET
    • POST
    • HEAD
  2. HTTP的头信息不能超出以下几种字段

    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type: 只限于三个值: application/x-www-form-urlencoded、multipart/form-data、text/plain

官方话的解读 ==>

简单请求是为了兼容表单(form), 因为历史上表单一直可以发出跨域请求。跨域的设计就是,只要表单可以发,AJAX和Fetch就可以直接发。

简单请求就是普通HTML Form在不依赖脚本的情况下可以发出的请求

  • 比如表单的method,可以是GET,POST。HEAD请求,这个确实没找出来为什么。(有大佬知道的话,可以在评论区留言告诉我)。
  • 表单的 enctype 属性可以设置上面提到的Content-Type的三个值。
  • http头信息的一些东西也都是表单请求发送是,请求头会自带的。

对于简单请求,浏览器直接发出CORS请求。但是,会在请求头信息中添加一个Origin字段。

实践出真知,来

const express = require('express');

const app = express();

app.post('/post', (req, res, next) => {
  console.log(req);
  res.status(200).json({
    code: '00000',
    data: {
      text: '你好',
    },
    message: '求收藏',
  });
});

app.get('/get', (req, res, next) => {
  res.status(200).json({
    code: '00000',
    data: {
      text: '祝好运',
    },
    message: '求点赞',
  });
});

app.listen(8000, () => {
  console.log('服务器已运行到http://localhost:8000');
});
fetch('http://localhost:8000/get', {
  method: 'get'
}).then(res => {
  console.log(res);
})
fetch('http://localhost:8000/post', {
  method: 'post'
}).then(res => {
  console.log(res);
})

很快啊,CORS error

但是,我们可以看到,Origin关键字已经在请求头中了,并且带着我们发出请求的网址。

那为什么还会报错呢?上面提到CORS策略是一个需要前后端一起配合的跨域解决方案。那么继续看

如果要解决简单请求的跨域问题,那么只需要,服务端在响应头中带一个Access-Control-Allow-Origin字段,表示服务端允许那些请求来源进行访问。浏览器会在请求返回的时候,核对是否有这个字段,发出响应的网址是否在允许访问的列表中,如果不满足上面两者,那么就会报错。

问题解决

一般比较常见的是直接将Access-Control-Allow-Origin的字段的值直接设为 *,表示允许全部来源的请求。

!注意
CORS请求默认不发送Cookie。如果要把Cookie发送到服务器,一方面要服务器同意,设置
Access-Control-Allow-Credentials字段,并设置值为 true
同时在请求时设置withCredentials字段,并设置值为true

对于fetch请求则略有不同。是通过配置credentials字段来实现。

上图

一开始,设置了cookie但是没有设置withCredentials.所以

请求并不携带cookie

打开,但是后端不设置Access-Control-Allow-Credentials

可以看见是请求头有cookie, 但是响应头没有Access-Control-Allow-Credentials。所以CORS error

后端设置Access-Control-Allow-Credentials

同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

这里我实践,发现get请求时,服务端不设置Access-Control-Allow-Credentials也可以接受到cookie,这块就不懂了(等后面明白了再补充)。

非简单请求

不满足简单请求的所有请求,都是复杂请求。

非简单请求的CORS请求,会在正式通信前,增加一次HTTP查询请求,称为"预检"请求。(preflight)

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就会报错。

对于为什么要添加这一次"预检请求",贺师俊老师写的文章中有明确的分析,这里我就不画蛇添足了。

juejin.cn/post/684490…

下面让我们在服务器端添加两个端口

前端也做一些修改

那么打开浏览器就可以看见

预检请求失败了,因为服务器端没有进行配合修改。

重点重提:注意加粗的黑体字
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就会报错。

来对服务器端进行配置

预检请求只发送一次,后面就可以每次直接请求,而不再询问服务器是否可以跨域了。

但是,我们会偶尔发现

为什么又多了一次预检请求?

原因在于每次预检请求有一个有效的时间限制。如下面的timeout = 5,表示有效时间为5s,如果超过5s,那么就需要再发送一次预检请求。

服务器代理请求

相对于CORS,服务器代理请求是我们现在日常开发中最常用的。

比如 webpack-dev-server, 通过利用http-proxy-middleware这个http代理中间件,实现请求转发给其他服务器。因为同源策略是浏览器的限制,服务器则没有。通过将请求代理到同源的本地服务器,巧妙的解决了同源策略的限制。

nginx反向代理接口跨域

Nginx 是一种高性能的反向代理服务器,可以用来轻松解决跨域问题。

反向代理拿到客户端的请求,将请求转发给其他的服务器,主要的场景是维持服务器集群的负载均衡,换句话说,反向代理帮其它的服务器拿到请求,然后选择一个合适的服务器,将请求转交给它。

server {
  listen  80;
  server_name  client.com;
  location /api {
    proxy_pass server.com;
  }
}

Nginx 相当于起了一个跳板机,这个跳板机的域名也是client.com,让客户端首先访问 client.com/api,这当然没有跨域,然后 Nginx 服务器作为反向代理,将请求转发给server.com,当响应返回时又将响应给到客户端,这就完成整个跨域请求的过程。

JSONP

这个感觉有点不常用了已经。

为什么会发现POST请求发送了两次

了解了上面的一些基础的常备知识,这里提出一个疑惑🤔,为什么我们上面说POST请求是简单请求,但是,我们却经常会发现POST请求会发送一次OPTIONS请求?是我上面在瞎说吗?

有大佬一眼就看出来的,就可以关闭这个傻呼呼的博文了(ps:还是多看一下验证验证,或者给小白提一点其他的见解,不胜感激)

上面我们说到,满足两个条件就可以被称为简单请求,那么我们可以去看一下

image.png

这里是掘金的文章列表接口,大家可以边看边验证。这里我代大家点开,具体的请求结构如下:

image.png 这里我们可以清晰的看到,Content-Type这个请求体中的字段,它的值明显不在我们

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain 这三种之中,所以,其实对于这个请求来说,这个POST请求是一个复杂请求。

⚠️ 注意,我这里说的是 这个POST请求

那么它多发一个预检请求就不足为奇啦。(来自2023年8月22日的补充)

结语

写这篇文章之前,对很多东西都一知半解,可能现在也是,但是也是真正的感受到了自己的知识版图被补全。
本文写之前,看了很多资料,也敲了很多代码,然后才回头进行的总结。耗时一天半(中间还得干其它活)。
感谢观看,要是感觉对你有所帮助,望点赞收藏。

祝,诸君武运昌隆。

参考资料

学习参考资料

问题解决资料