跨域问题很常见,但解决跨域的方法却异常简单,10分钟足以彻底掌握!!!
后端
为了更加贴近真实的开发场景,这里我先基于 koa 来搭建一个后端服务,并运行在 localhost
的 8989
端口上,由于代码比较简单,这里就不展开说了,有兴趣的可以自行了解学习 koa 的使用
服务启动之后,访问 http://localhost:8989/user/list
即可正常获取接口响应的数据
前端
接着使用npm create vue@latest
快速初始化一个前端项目,移除掉多余无用的文件并通过原生的 fetch 来进行接口请求,如果不熟悉 featch ,你也可以自行安装 axios ;通过npm run dev
启动该项目,启动成功之后会运行在localhost:5173
,如下所示:
完美!!!
当你满心欢喜的在浏览器打开 localhost:5173
,恐怖的一幕发生了
焯!!!
不难发现,请求失败了,原因(翻译)如下:
从源 'http://localhost:5173' 获取 'http://localhost:8989/user/list' 的访问被 CORS 策略阻止:请求的资源上没有'Access-control-allow-origin'标头。如果不透明的响应满足您的需求,请将请求的模式设置为'no-cors'以获取禁用CORS的资源。
根据错误提示来看,解决办法,非常简单!
- 如果客户端需要确确实实越过这个问题拿到真实的响应,那么你直接找到服务端让他在响应头中添加如下配置 Access-control-allow-origin 允许你访问即可
- 如果客户端不关心响应的结果,那么可以在 fetch 请求中配置 mode 参数来规避掉这个问题,但这样的请求是一个不透明的请求(Opaque Request),对于客户端而言响应是不可解读的,通常我们不会这么去做!
cors
从上面的错误提示可以看到,当响应头中缺少 Access-control-allow-origin 字段时,响应会被 cors 策略阻止,那么是什么是 cors 呢?
跨源资源共享(CORS,Cross-Origin Resource Sharing) ,通常也被叫做跨域资源共享 ,是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其他源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源
出于安全性考虑,浏览器会限制脚本内发起的 跨源 HTTP 请求,即 XMLHttpRequest 和 Fetch API 都需要遵循 同源策略 。这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源,除非响应头中包含了 Access-control-allow-origin
需要明确的是:此刻的请求客户端已经正常发出了,服务端也正常响应了,但是浏览器没有在响应头中看到 Access-control-allow-origin 字段,所以将请求进行了拦截!
那么什么是 同源 ,什么是 跨源 呢?
URL
一个完整的 URL 通常会由以上部分进行组成,我们重点关注 scheme(协议) 、domain name(主机名) 、port(端口号),从三者是否完全相同即可判断一个发出请求的源和处理请求的源是否是跨源请求
同源
协议、主机名、端口号 完全相同
跨源
协议、主机名、端口号 有任意一个不同
常见解决方案
1. 前端项目设置 proxy
当前我们在 vue 项目下进行开发,打包工具为自带的 vite ,可以利用其 server 配置 proxy 实现;将代码还原为最初的样子,在项目根目录下找到 vite.config.js
proxy 的配置非常简单,key 通常是一个自定义的 字符串 或者 正则表达式 ,是在本地开发时添加的一个虚拟路径,这里配置为 /api
,当请求发起时,所有请求路径中包含 /api
的请求都会使用该规则代理到配置的源(也就是这里的 target ,即为服务端接口的真实源);changeOrigin 用于设置是否将请求的 host 修改为 target , 从而使目标服务器认为请求是从同一个源发起的,细心的你可能会发现,无论你将它修改为 true
还是 false
,host 在浏览器上都始终显示为 localhost:5173
, 但是,如果你从服务端获取 host 时就会发现区别
rewrite 就更简单了,将你请求路径中匹配的 /api
重写为空字符串,从而向真实的接口进行请求。
但是,这种方式存在一个问题,就是仅针对开发环境 ;如果是生产环境 ,这段代码是无效的,你之前在请求路径前面添加的 /api
最终会携带请求至远程而导致请求失败;因此在生产环境时,你需要移除掉路径前的 /api
,或者联系服务端在接口路径前统一添加 /api
,这样你就无需任何处理了
完美解决!!!
扩展 :同源策略的拦截主要发生在浏览器上出现的跨源请求,而服务器之间的相互请求并不存在跨域的问题,如果说你想更深层次的去探究一下 http-proxy 的使用,那么你会发现在服务端可能有很多办法去解决跨域的问题,比如:
- 可以再搭建一个 node 服务(中间件)作为代理服务器,用于转发前端请求并从真实的服务器中获取请求
- 基于 nginx 的反向代理来进行跨域处理
代理的本质并不复杂,但是需要掌握一丢丢服务器相关的知识,有兴趣可以自行尝试
2. 服务端设置 CORS
当浏览器中出现跨源错误提示,最简单的方式就是在响应头中添加 Access-control-allow-origin,上面我们也单独在请求的响应头中进行了设置,但真实开发中我们通常会统一进行设置。先将代码进行还原,然后编写如下配置:
仅允许特定的源进行访问
切换到 vue 项目目录再启动一个
此时,运行在 http://localhost:5174
就没有办法进行访问
这个也很好解决,代码如下
除此之外,这部分可能会涉及到 简单请求 和 复杂请求 在请求表现时的区别,主要在于浏览器是否会在请求前发送一个预检请求(方法为 OPTIONS )来明确服务端是否支持该请求跨域,但更多的设置都是在服务端进行,对本文没有任何影响,有兴趣的话可以自行了解,这里我们不妨简单再看一下,直接来个 delete 请求来作为复杂请求举个例子
此时请求方法已被更改为 delete ,跨域问题也已处理,当用户点击 button 即可发送请求,打开 network 面板
在 delete 请求发送前,会预先发送一次预检请求 用于确定服务器是否支持该方法跨域,支持才会接着发送 delete 请求用于获取请求结果;如果不支持,则会阻止该 delete 请求继续发送
比如服务端只允许 get 请求时,当你点击按钮发送 delete 请求就会的错一个热乎的错误提示
预检请求发现 delete 方法不被允许,因此被浏览器就会阻止该请求继续发送了
接着我们分别来看一下 简单请求 和 复杂请求 被 cors 策略拦截时有什么区别
简单请求:请求发送了也响应了,但是被浏览器的同源策略拦截了!
复杂请求:发送了预检请求,但是发现服务端不给你访问,因此浏览器也会阻止你进一步发送该请求
更多响应配置可以参考 CORS
3. 前端 + 服务端 JSONP
JONP(JSON with Padding) 这种方式现阶段已经很少使用了,基本属于属于上古遗物的状态,但是有必要做个简单的了解。由于浏览器的同源策略,普通的跨域请求是会受到限制的,但 script 标签却可以绕过这样的限制从而去请求远程的资源
将所有的代码还原之后,在服务端添加一个新的请求 http://localhost:8989/user/json
,并在 vue 项目下的 index.html 中通过 script 的方式去加载远程资源
此时由于移除的跨域的处理代码,所以原有的 fetch 请求仍然会被进行拦截;而反观通过 script 进行加载的内容却正常在控制台进行了执行,打印出了对应的内容
所以我们就可以利用这个特点,在服务端将需要返回的 json 数据进行拼接,这样当 script 的 src
访问该请求时即可获取正常的响应,但这种方式也仅仅只适用于 get 请求
此时浏览器的控制可以正常打印相应数据,但是前端如何通过 js 去操作这份数据呢?最好的方式,就是前端和服务端共同去约定一个方法,前端请求的时候将方法通过参数传递给服务端,服务端对参数进行解析取出该方法,然后通过 js 的方式调用该方法并将响应的数据作为参数进行传递即可