好久没更博了,今天被学弟问到了关于Vue请求浏览器跨域报错的问题,于是来水一篇吧!

最近被搬运站搞怕了,在此提前声明,本站全部文章均属个人知识分享,未经授权禁止全文搬运或使用计算机技术爬取到其他商业站点。为维护国内良好知识分享环境,在正常引用时请注意标清本文地址。本文作者依法保留追究违规或非法使用本文内容的权利,望各位同仁相互理解。

先看报错,跨域报错一般在控制台以如下方式发出警告:Access to XMLHttpRequest at 'http://站点IP或域名:端口号/接口路径?参数=参数值' from origin 'http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

简单机翻后是:从源http://localhost:8080访问http://请求地址(此处略写)上的XMLHttpRequest已被CORS策略阻止:对预检请求的响应未通过访问控制检查:请求的资源上不存在Access-Control-Allow-Origin标头。

看关键词,CORS策略预检请求

什么是CORS策略

CORS 是一个 W3C 标准,全称 Cross-origin Resource Sharing,中文名称 “跨域资源共享”,它突破了一个请求在浏览器发出只能在同源的情况下向服务器获取数据的限制[1]。它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制[2]

什么叫同源?同源即地址里的域名协议端口号均相同[3]

三个条件只要有一个不满足,就叫做跨域[4]

  • 举几个栗子[5]
    1. http://www.funtl.com –> http://admin.funtl.com 跨域,域名不相同
    2. http://www.funtl.com –> http://www.funtl.com 非跨域,站点相同
    3. http://www.funtl.com –> http://www.funtl.com:8080 跨域,端口不相同
    4. http://www.funtl.com –> https://www.funtl.com 跨域,协议不相同(http和https)

我们本地的vue项目运行起来,页面地址一般是localhost,这个时候请求后台的接口地址(无论是服务器还是后端小伙伴的电脑),肯定就会存在跨域问题。

CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10。整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信(在 header 中设置:Access-Control-Allow-Origin)。[2]

为什么要有同源策略

世间万物都有其存在的理由。

同源策略的出现主要目的是为了安全,它是浏览器最核心最基本的安全功能。

  • 同源策略的限制:

    1. Cookie、LocalStorage 和 IndexDB 会话存储无法读取。
    2. DOM 无法获得。
    3. AJAX 请求不能发送。
  • 同源策略作用:

    1. 防止恶意网页可以获取其他网站的本地数据。
    2. 防止恶意网站iframe其他网站的时候,获取数据。
    3. 防止恶意网站在自已网站有访问其他网站的权利,以免通过cookie免登,拿到数据。

如果没有同源限制,在浏览器中的cookie等其他数据可以任意读取,不同域下的DOM任意操作,ajax任意请求其他网站的数据,包括隐私数据。

什么是预检请求

在本地调试Vue页面时,会在开发者工具的网络请求中经常发现有同一个请求发送两次的现象,这是由于发生了跨域。

跨域资源共享标准新增了一组HTTP首部字段,允许服务器声明 A源站 通过浏览器有权限访问 B资源。规范要求,对那些可能对服务器资源产生副作用的HTTP请求方式,在发送实际的请求之前,浏览器或客户端必须使用OPTTIONS发起一个预检请求(preflight request),从而获知服务器端是否允许该跨域请求。服务器确认允许后,才发起实际的HTTP请求,这样可以避免跨域请求对服务器的用户数据造成影响。说白了,就是要先发起一个验证,看是否符合要求。与HTTP三次握手,有一点点类似[1][6]

什么时候会触发预检请求

但并不是所有的跨域请求都会触发preflight request预请求。

简单请求

满足如下【所有条件】,即使跨域,也不会触发预检请求,一般称为简单请求[7]

  1. 使用 GETHEADPOST 方法之一
  2. 除了被用户代理自动设置的首部字段(例如 ConnectionUser-Agent或其他在 Fetch 规范中定义为 “禁用首部名称” 的首部),允许人为设置的字段为 Fetch 规范定义的 “对 CORS 安全的首部字段集合”。该集合为:AcceptAccept-LanguageContent-LanguageContent-Type。意思就是说,你没有随意添加其他自定义头部信息。
  3. 注意上一条提到的 Content-Type 的值只能是 text/plainmultipart/form-dataapplication/x-www-form-urlencoded三者之一
  4. 请求中的任意 XMLHttpRequest 对象均没有注册任何事件监听器;XMLHttpRequest 对象可以使用 XMLHttpRequest.upload 属性访问。
  5. 请求中没有使用 ReadableStream 对象。

注意是满足以上所有,所有条件哦!才属于简单请求,不会触发预检。

非简单请求

与前述简单请求不同,“需预检的请求”要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。“预检请求”的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

和上面的条件对比一下可知,当请求满足下述任一条件时,即应首先发送预检请求[6]

  1. 使用如下任一Http请求方式:PUTDELETECONNECTOPTIONSTRACEPATCH
  2. 设置了对 “CORS安全的首部字段集合” 之外的首部字段,集合如下:AcceptAccept-LanguageContent-LanguageContent-TypeDPRViewport-WidthWidthDownlinkSave-Data
  3. Content-Type 的值不属于 text/plainmultipart/form-dataapplication/x-www-form-urlencoded三者的任何一个

如何解决跨域问题

首先可以让后端小伙伴来解决,让他配置接受跨域,实现跨域也可以正常访问接口!比如,如果后端是Node.js,可以看看这篇文章 跨域(CORS)产生原因分析与解决方案,这一次彻底搞懂它,这个作者提供了三种解决方法:使用CORS模块JSONPNginx代理服务器配置跨域。如果后端是其他,你让他自行去找一下解决办法。

什么,他也不会?他听不懂什么是同源策略?那好吧,还得靠咱自己,以下是前端使用Vue-Cli配置跨域的解决方案。

我们前端解决本地开发环境的方法一般是配置proxy反向代理,步骤如下[8]

配置axios请求根路径

在你导入和挂载axios的地方修改原有的请求根路径axios.defaults.baseURL,我在项目中一般是写到了 main.js 中:

1
2
3
4
5
6
//文件main.js

import axios from 'axios'
//axios.defaults.baseURL = 'https://xxx.xxx.xxx' // 原有的API请求根路径
axios.defaults.baseURL = '/api' // 修改成这一行
axios.defaults.timeout = 5000

在开发/测试环境中,因为会使用proxy方法,所以此处的baseURL不太重要,它在proxy中会被替换,写成别的名字也没事,不用非得叫/api,但下一步你也需要做一些调整哦!

配置devServer

vue-cli3中将扩展和基础配置都写在根目录的vue.config.js中。vue.config.js 是一个可选的配置文件,如果项目的 (和 package.json 同级的) 根目录中存在这个文件,那么它会被 @vue/cli-service 自动加载。[9]

devServer用于本地开发环境,生产环境是不生效的。[3]生产环境里前端应用和后端API服务器一般就运行在同一个主机上了,就不用代理来跨域了。详情请见Vue CLI官方文档

  • 在下面的代码里:
    1. proxy会拦截所有url中可以成功匹配到 /api 的请求。(proxy采用正则匹配,一旦url中包含你要的字符串则停止向下匹配,详查proxy匹配规则)
    2. 它会把拦截到的请求中的baseUrl替换为此处的target
    3. changeOrigin:true 表示跨域
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//文件vue.config.js

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
publicPath: './',
// 下方代码为配置proxy解决跨域问题
devServer: {
proxy: {
// 代理标识,也是请求前缀,标明哪些连接需要使用代理,只有有标识的连接才用代理
// '/api'就是告知,接口以'/api'开头的才用代理,所以写请求接口时要使用'/api/xx/xx'的形式,使用代理后生成的请求路径就是像'http://xxx.xxx.xxx/api/xx/xx'这样的
'/api': {
target: 'https://xxx.xxx.xxx', // 想让它替换成的后端接口请求实际的根路径,比如localhost:3000 => https://xxx.xxx.xxx
changeOrigin: true, // 开启跨域,是否修改host请求的域名为目标域名
ws: true, // 是否启用proxy websockets
// 路径重写,标识替换,也就是说会修改最终请求的API路径。
// 比如原请求地址为'/api/getData', 将'/api'替换''时,最终代理后访问的路径为:http://xxx.xxx.xxx/getData,这个参数的目的是给代理命名后,在访问时把命名删除掉。
pathRewrite: {
'^/api': '' // 匹配以/api为开头的路径都重写为空字符串
}
}
}
}
})

其中对于pathRewrite路径重写,它是和target打配合使用的。

比如你起的一个代理标识叫/api,接口以它开头的才用代理,此代理target值你设置为了http://xxx.xxx.xxx:8888。当你发起axios请求时,比如请求/api/getData时就被拦截添加了target的值(请求根路径),即生成的请求路径变成了http://xxx.xxx.xxx:8888/api/getData

如果你想要的实际请求路径中没有/api这几个字符呢?这就需要pathRewrite与之配合了。将pathRewrite值重写成空字符串'^/api': ''即可,重写后请求地址就变成了你想要的http://xxx.xxx.xxx:8888/getData

案例演示

配置axios请求根路径,如下配置后,所有的axios请求开头都将带上/api

1
2
3
4
5
6
//文件main.js

import axios from 'axios'
//axios.defaults.baseURL = 'http://127.0.0.1:4523/m1/1766-0-default' // 原有的API请求根路径
axios.defaults.baseURL = '/api' // 修改成这一行,所有的axios请求开头都将带上/api
axios.defaults.timeout = 5000

配置devServer.proxy,如下配置后,所有开头为/api的请求都将被代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//文件vue.config.js

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
publicPath: './',
// 下方代码为配置proxy解决跨域问题
devServer: {
proxy: {
// 代理标识,所有开头为/api的请求都将被代理
'/api': {
target: 'http://127.0.0.1:4523/m1/1766-0-default', // 原有的API请求根路径
changeOrigin: true, // 开启跨域,修改host请求的域名为目标域名
ws: true, // 是否启用proxy websockets
pathRewrite: {
'^/api': ''
}
}
}
}
})

在Vue组件页面中发送请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 获取文章索引列表数据
methods: {
async getList() {
const { data: res } = await this.$http.get('/article/getIndex', {
params: {
type: 'notice',
pageNum: this.currentPage,
pageSize: this.pageSize
}
})
// console.log(res)
if (res.meta.status !== 200) return this.$message.error(res.meta.message)
this.noticesList = res.noticeList
this.total = res.total
}
}

发送请求所经历的过程,大概简述(假设页面运行在localhost:8080):

  1. 请求地址 /article/getIndex 被添加上了axios请求根路径 /api,变成了 /api/article/getIndex
  2. proxy代理匹配到了字符 /api,为它添加上了target的值(真实请求基准地址),变成了 http://127.0.0.1:4523/m1/1766-0-default/api/article/getIndex
  3. pathRewrite路径重写,将请求地址中的/api替换成空字符串,实际请求地址变成了 http://127.0.0.1:4523/m1/1766-0-default/article/getIndex
  4. 为了伪造同源请求,浏览器中并不直接请求实际地址,而是将请求发送到了反向代理服务器,因此开发者工具中看到的请求地址是http://localhost:8080/api/article/getIndex。随后反向代理服务器向实际的请求地址发送请求http://127.0.0.1:4523/m1/1766-0-default/article/getIndex并将结果返回。

也就是说,页面本地运行在localhost:8080,发送请求时浏览器的请求地址也是localhost:8080,这样就欺骗了浏览器,成功伪造了同源请求,从而解决了跨域报错。

其实也就是让反向代理服务器作为中间人,帮我们跑了个腿。让它去请求真正的接口地址,这样就绕过了我们直接去请求这个真正地址而导致得跨域问题[10][11]

注意:

  1. devServer.proxy 提供的代理功能,仅在开发调试阶段生效
  2. 项目上线发布时,可能依旧需要API接口服务器开启 CORS 跨域资源共享

参考内容

[1] https://zhuanlan.zhihu.com/p/179281276

[2] https://www.jianshu.com/p/cfbccb0adae9

[3] https://www.jianshu.com/p/7d40c2888874

[4] https://blog.csdn.net/qq_25170255/article/details/123003651

[5] https://www.jianshu.com/p/cfbccb0adae9

[6] https://www.cnblogs.com/xianrongbin/p/10447159.html

[7] https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS#简单请求

[8] https://blog.csdn.net/a949370771/article/details/120834790

[9] https://cli.vuejs.org/zh/config/#vue-config-js

[10] https://blog.csdn.net/my_csdn2018/article/details/82909989

[11] https://blog.csdn.net/qq_42345237/article/details/99690356