该是重新点亮星星的时候了✨✨
前言
感觉跨域是一个老生常谈的问题了。想象一下下面的场景,相信大家都经历过。
// 场景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通信请求,浏览器分为两类
- 简单请求
- 非简单请求
简单请求
比较官方的说法 ==>
只要同时满足以下两大条件,就属于简单请求
-
请求方法是以下三种方法之一
- GET
- POST
- HEAD
-
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请求,否则就会报错。
对于为什么要添加这一次"预检请求",贺师俊老师写的文章中有明确的分析,这里我就不画蛇添足了。
下面让我们在服务器端添加两个端口
前端也做一些修改
那么打开浏览器就可以看见
预检请求失败了,因为服务器端没有进行配合修改。
重点重提:注意加粗的黑体字
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些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:还是多看一下验证验证,或者给小白提一点其他的见解,不胜感激)
上面我们说到,满足两个条件就可以被称为简单请求,那么我们可以去看一下
这里是掘金的文章列表接口,大家可以边看边验证。这里我代大家点开,具体的请求结构如下:
这里我们可以清晰的看到,
Content-Type这个请求体中的字段,它的值明显不在我们
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain 这三种之中,所以,其实对于这个请求来说,这个POST请求是一个复杂请求。
⚠️ 注意,我这里说的是
这个POST请求
那么它多发一个预检请求就不足为奇啦。(来自2023年8月22日的补充)
结语
写这篇文章之前,对很多东西都一知半解,可能现在也是,但是也是真正的感受到了自己的知识版图被补全。
本文写之前,看了很多资料,也敲了很多代码,然后才回头进行的总结。耗时一天半(中间还得干其它活)。
感谢观看,要是感觉对你有所帮助,望点赞收藏。
祝,诸君武运昌隆。
参考资料
学习参考资料
- developer.mozilla.org/zh-CN/docs/… MDN 浏览器的同源策略
- www.ruanyifeng.com/blog/2016/0… 跨域资源共享CORS详解
- blog.csdn.net/besto229/ar… CORS跨域
- github.com/amandakelak… CORS 简单请求+预检请求
- juejin.cn/post/684490… 奇舞团 为什么要区分简单请求和预检请求
- zhuanlan.zhihu.com/p/141977678 解决跨域的几种方法
问题解决资料
- blog.csdn.net/letterTiger… Chrome不显示OPTIONS请求的解决方法