说一说跨域和如何解决

前言

跨域问题是我们在面试过程中经常容易被询问到的,今天我们来聊聊什么是跨域以及如何解决跨域。

我们先来聊聊跨域是什么?

比如说我们去访问百度的首页https://www.baidu.com/,那么浏览器就会朝这个地址去发送一次网络请求。我们来看看这个地址是由哪几个部分组成的:

首先百度是对这段地址的域名去做了一个域名优化的,原来的地址是有端口号的,这才是一个正常的http地址,这里我们就假设为:

https://192.168.31.45:8080/user

这段地址分为四个部分:

协议号:域名:端口号 / 路径

现在我们来思考一个问题,如果该地址是百度的后端服务器的ip地址,只要我们朝该地址发请求,我们是否可以拿到数据?

如果真的可以随便拿到的话,那是不科学的。那么这些大公司的数据将毫无秘密可言。那么为了防止这个问题,所有浏览器都打造了一个同源策略

同源策略

协议号-域名-端口号 都相同的地址,浏览器才认为是同源

  • https://192.168.31.45:8080/user
  • https://192.168.31.45:8080/list

我们来看看这两个地址,它们是同源地址吗?

是的,它们是同源地址,它们协议号-域名-端口号都相同,只是路径不一样。

公司的ip都是公网ip,所以公司之间的ip是绝对不一样的,192.168.31.45这一段数字不可能相同。所以说,百度的程序员不可能去请求到腾讯的后端数据。

如果它们的协议号-域名-端口号有任何一个不相同,那么浏览器就会将返回的数据拦截下来,这就是跨域。

跨域

后端返回给浏览器的数据会被浏览器的同源策略给拦截下来,这就是跨域

假设百度的前端和字节的前端都去访问字节的后端,首先后端只要有请求都会响应,所以会出现很尴尬的情况。字节的后端同样会返还东西给百度的前端,浏览器就做了同源策略。如图所示,假设它们的地址为这样,由于百度的前端和字节的后端的协议-域名-端口号三者不是完全相同,那么浏览器则将后端返回回来的响应拦截下来,并不会发给前端,这就是跨域

image.png

这里需要注意一下,大公司的ip地址是不会一样的,这是它们在万维网申请的,是全球唯一的。

同源策略的目的是数据安全被浏览认为不是同一个源的请求,就拿不到响应

解决跨域

为什么要解决跨域呢?

假设我在我们公司是干前端的,我们公司有个哥们是干后端的。我们两个负责搭配完成一个全栈项目。假设我把我的前端项目跑在http://192.168.31.1:8000,后端那个哥们把后端跑在http://192.168.31.2:8000

虽然我们都连的是公司的局域网,但是域名最后一个数字还是会不一样的。

前端需要朝后端发送接口请求,那这样能请求到数据吗?答案是不能的,因为发生了跨域!所以就需要我们解决跨域,让我们在开发阶段好调试。

常用的解决跨域的办法有四种,我们需要掌握。

我们用代码来给大家演示一下:

我们来看看后端代码:

 

js

复制代码

const Koa = require('koa'); const app = new Koa() const main = (ctx, next) => { ctx.body = { data: 'hello world' } } app.use(main) app.listen(3000, () => { console.log('listening on port 3000'); })

后端跑在localhost:3000上面。返回数据hello world

我们再来看看前端代码:

首先我们来看看前端的代码:

 

html

复制代码

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button id="btn">获取数据</button> <!-- <script src="http://localhost:3000"></script> --> <script> let btn = document.getElementById('btn'); btn.addEventListener('click', () => { // 发请求 fetch('http://localhost:3000') .then(res => res.json()) .then(res => { console.log(res); }) }); </script> </body> </html>

我们想要实现当我们点击按钮时,前端朝后端发请求,拿到数据并打印。

我们点击一下按钮试一试:

image.png

瞧,报错了,因为受到了浏览器的同源政策保护,跨域了,后端返回的响应被浏览器劫持了。

1. JSONP

首先我们要明白一点, ajax请求受同源策略的影响,但是script上的src属性不受同源策略的影响,且该属性也会导致浏览器发送一个请求。

 

html

复制代码

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button id="btn">获取数据</button> <script src="http://localhost:3000"></script> <script> let btn = document.getElementById('btn'); btn.addEventListener('click', () => { }); </script> </body> </html>

我们通过在script中添加src属性也会发送一段请求,它是不受同源政策影响的:

image.png

这样,是不会发生跨域的。可能有些小伙伴们就想到了,我们有时候会通过CDN引入第三方源码,我们这样引入也没有报错, 比如直接引入vue:<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

如果通过script去访问资源时也受同源策略影响,那么我们就没有办法引入第三方的库了。

现在我们就来通过这个script去拿到数据:

 

html

复制代码

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button id="btn">获取数据</button> <!-- <script src="http://localhost:3000"></script> --> <script> function jsonp(url, cb){ return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = `${url}?cb=${cb}` // http://localhost:3000?cb=`callback` document.body.appendChild(script); }) } let btn = document.getElementById('btn'); btn.addEventListener('click', () => { // 发请求 jsonp('http://localhost:3000', 'callback') .then(res => { console.log('后端返回的结果' + res); }) }); </script> </body> </html>

我们自己封装一个jsonp函数来用于发接口请求,这个jsonp函数可以接受urlcb作为参数。首先生成一个<script>标签,并且给script添加一个src属性,值为拼接后的urlcb,然后将该script标签放到body当中去,这样我们就确保了该jsonp函数可以利用script标签去发生请求。

前端代码就先写到这,我们点击按钮来测试一下:

image.png

我们成功的发送了一个请求。既然前端发送了一个请求给后端,那么后端就一定去接收到了请求。现在我们来到后端,后端应该成功接收到前端传过来的cb字符串。我们到后端中打印这个参数来看一下:

 

js

复制代码

// const Koa = require('koa'); // const app = new Koa() // const main = (ctx, next) => { // console.log(ctx.query); // { cb: 'callback' } // const cb = ctx.query.cb // const data = '给前端的数据' // const str = `${cb}('${data}')`; // 'callback("给前端的数据")' // ctx.body = str // } // app.use(main) // app.listen(3000, () => { // console.log('listening on port 3000'); // }) const Koa = require('koa'); const app = new Koa() const main = (ctx, next) => { console.log(ctx.query); ctx.body = { data: 'hello world' } } app.use(main) app.listen(3000, () => { console.log('listening on port 3000'); })

image.png

 

js

复制代码

const Koa = require('koa'); const app = new Koa() const main = (ctx, next) => { console.log(ctx.query); // { cb: 'callback' } const cb = ctx.query.cb const data = '给前端的数据' const str = `${cb}('${data}')`; // 'callback("给前端的数据")' ctx.body = str } app.use(main) app.listen(3000, () => { console.log('listening on port 3000'); })

前端传了一个单词给我们后端,然后我们后端将给我的这个单词拼接为一个字符串再返回给前端。这里使用的是es6的模板字符串。大家可以看看代码上的注释,更好理解。我们返回给前端的str像不像一个函数的调用呢?

我们再来看前端:

我们在全局的window对象上挂上这个参数cb,属性为该参数值,值为一个函数体。注意,我们这里只是声明,并没有调用。

 

html

复制代码

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button id="btn">获取数据</button> <!-- <script src="http://localhost:3000"></script> --> <script> function jsonp(url, cb){ return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = `${url}?cb=${cb}` // http://localhost:3000?cb=`callback` document.body.appendChild(script); window[cb] = (data) => { console.log(data) } // callback() // { // "callback": () => {} // } }) } let btn = document.getElementById('btn'); btn.addEventListener('click', () => { // 发请求 jsonp('http://localhost:3000', 'callback') .then(res => { console.log('后端返回的结果' + res); }) }); </script> </body> </html>

到这里,我们就已经可以看到效果了,我们点击按钮来看看打印:

image.png

我们的前端成功的拿到了后端的数据,这里有打印是因为我们挂载在window上的cb函数触发了。但是我们前端并没有去触发它,那么就是后端来触发的,并且将我们想要的数据作为参数来传给函数,这样才能打印出来数据。

我们来看看这个函数是怎么触发的:

 

js

复制代码

const main = (ctx, next) => { console.log(ctx.query); // { cb: 'callback' } const cb = ctx.query.cb const data = '给前端的数据' const str = `${cb}('${data}')`; // 'callback("给前端的数据")' ctx.body = str }

前端将cb挂在window上,并且将cb传给后端,后端就收到cb,然后使用字符串模板进行拼接,使其变成一个函数的调用,将给前端的数据作为此函数的参数:'callback("给前端的数据")'。然后将这个字符串传给前端,浏览器会将字符串执行成callback的调用。

我们来看看后端的响应:

image.png

我们将console.log换成resolve,这样后面的.then即可拿到后端的数据:

 

html

复制代码

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button id="btn">获取数据</button> <!-- <script src="http://localhost:3000"></script> --> <script> function jsonp(url, cb){ return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = `${url}?cb=${cb}` // http://localhost:3000?cb=`callback` document.body.appendChild(script); window[cb] = (data) => { resolve(data) } // callback() // { // "callback": () => {} // } }) } let btn = document.getElementById('btn'); btn.addEventListener('click', () => { // 发请求 jsonp('http://localhost:3000', 'callback') .then(res => { console.log('后端返回的结果:' + res); }) }); </script> </body> </html>

image.png

我给大家画张图来看一下:

image.png

  1. 借助script的src属性给后端发送一个请求,且携带一个参数('callback')

  2. 前端在window对象上添加了一个 callback 函数

  3. 后端接收到了这个参数'callback'后,将要返回前端的数据data和这个参数 'callback' 进行拼接成'callback(data)',并返回

  4. 因为window上已经有一个callback 函数,后端又返回了一个形如'callback(data)',浏览器会将该字符串执行成``callback 的调用`

总结

  1. ajax请求受同源策略的影响,但是<script>上的src属性不受同源策略的影响,且该属性也会导致 浏览器发送一个请求

缺点

  1. 必须要后端配合
  2. 只能用于get请求

jsonp是第一种解决跨域的常见手段,接下来我们来看看第二种:

Cors (Cross-Origin Resource Sharing)

我们上面提到,跨域是因为浏览器的同源政策,导致浏览器不接受(或者说拦截)后端的响应,那么Cors就是让浏览器不得不接受响应。

http协议中,任何一个http请求都由两部分组成,一个是请求头,一个是请求体。请求头放着关于此次http请求的描述信息,请求体中装着传递的参数及数据包。

后端返回的是响应头和响应体,我们在响应头中添加一个字段'Access-Control-Allow-Origin':'*'

这相当于设置一个白名单,告诉浏览器不要拒绝接受后端的响应,让浏览器认为是一个同源。

 

js

复制代码

const http = require('http'); const server = http.createServer((req, res) => { // 跨域是浏览器不接受后端的响应 // 想个办法,让浏览器不得接受 res.writeHead(200, { 'Access-Control-Allow-Origin': '*' // 白名单 }) let data = { msg: "hello cors" } res.end(JSON.stringify(data)) // 向前端返回数据 }) server.listen(3000, () => { console.log('listening on port 3000'); })

再来看看前端的代码,前端的代码还是没有变化:

 

html

复制代码

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <button id="btn">获取数据</button> <script> let btn = document.getElementById('btn') btn.addEventListener('click', () => { fetch('http://localhost:3000', 'GET') .then(res => json()) .then(res => { console.log(res) }) }) </script> </body> </html>

当我们点击按钮:

image.png

成功的拿到后端返回的数据。

那小伙伴们可能又会有疑问了,这样设置白名单的话,是不是所有的前端都可以访问我们的后端了?

这是因为我们实际在开发自己项目中,我们会偷懒,'Access-Control-Allow-Origin': '*',设置为 * 号,相当于全部地址设置为白名单。

但是我们应该写成自己前端的ip地址,例如我们这里写成'Access-Control-Allow-Origin': 'http://127.0.0.1:5500'。这样的话,我们的前端就能正常的访问后端,同样可以限制别人的前端来访问我们的后端。

总结

Cors (Cross-Origin Resource Sharing) --- 后端通过设置响应头来告诉浏览器不要拒绝接受后端的响应

node代理

node代理也是我们常用的一种解决跨域的手段。

我们就拿vue来举例一下,在我们写vue的项目时,可以使用一个node代理来解决跨域问题。我们用vite来创建一个后端项目

 

js

复制代码

<template> </template> <script setup> import axios from 'axios' import { onMounted } from 'vue' onMounted(() => { axios.get('http://localhost:3000') .then((res) => { console.log(res); }) }) </script> <style scoped> </style>

我们实现一个当一进去页面时,就朝后端发接口请求拿到数据。这里请求我们写在挂载阶段onMounted当中,当组件挂载完毕后就会触发回调函数,发送请求。

这里我们使用的是axios,我们需要安装一下依赖npm i axios

后端代码:

 

js

复制代码

const http = require('http'); const server = http.createServer((req, res) => { let data = { msg: "hello cors" } res.end(JSON.stringify(data)) // 向前端返回数据 }) server.listen(3000, () => { console.log('listening on port 3000'); })

现在还是跨域的,因为我们前端和后端的地址不是同源的,它们的端口号不一样。

image.png

接下来我们就来到vite的配置项来解决跨域问题

vite.config.js

使用vite创建的项目是有一个vite.config.js的文件的:

image.png

 

js

复制代码

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], server: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } } })

来给大家解释一下这个vite的配置项是什么样的:

首选vite的源码是用node来写的,server是和网络请求相关的配置,然后我们进行一个代理proxy,只要我们朝/api这个路径发送请求时,例如:axios.get('/api'),那么就会将它转发到target这个路径下来。如果后端本就有/api的路径的话,那么就帮我们去掉。

我们改动一下前端代码:

 

js

复制代码

<template> </template> <script setup> import axios from 'axios' import { onMounted } from 'vue' onMounted(() => { axios.get('/api') .then((res) => { console.log(res); }) }) </script> <style scoped> </style>

再来页面输出看看:

image.png

看,我们成功拿到了数据。

我来给大家解释一下,同源策略只在浏览器上有,后端上是没有的。假设我们想要拿到气象局的天气预报数据,那么我们前端朝气象局的后端发送请求,那么一定是会跨域的。如果我们用node写一个后端,在后端中朝着气象局的后端去发送请求,这个过程中不会经过浏览器,所以不会发生跨域。那么我只需要在后端中再使用一下Cors,前端就能到自己的后端中拿到气象局的数据,这就是node代理

我们上面的vite就是这么干的,vite帮我们启动了一个node服务,且帮我们朝着localhost:3000发起请求,因为后端没有同源策略,所以,vite中的node服务能直接请求到数据,再提供给前端使用。

注意,此时的vite还帮创建出来的后端Cors了一下的。

但是,vite只适合我们在开发阶段使用,因为vite只是一个在开发阶段使用的构建工具,我们所写的项目最后是要打包上线的,最后vite的整个源码都会被清除掉。

总结

因为后端没有同源政策,vite创建一个node服务,所以node可以直接请求到后端的数据,再拿给前端。

但是vite只能在开发环境生效。

nginx代理

nginx代理Cors的机制差不多,都是通过配置请求头中的字段来实现的,这需要在后端服务器上安装ngnix,然后进行一个配置,只有源和配置的相等后端才进行响应。ngnix不是js语法,而是Linux语法,属于操作系统的。

绝大多公司都是通过nginx代理去解决跨域,我现在没有很好的办法来给大家演示,它主要用于项目上线时区去解决跨域,如果以后我写有关于项目部署的文章,再来跟大家好好聊聊。大家只要了解nginx就行了,它的机制跟Cors差不多。

以上四种就是我们常见的解决跨域的方法,它足够我们进行任何开发。不过面试官可能问你你还知道别的手段吗?那就是一些不常用的跨域方法,我们来简单介绍一下:

不常用的解决跨域的手段

domain

首先我们要知道iframe的作用,它允许我们在一个html中可以去嵌套另一个html:

 

html

复制代码

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <h2>父级页面</h2> <iframe src="./child.html" frameborder="0"></iframe> </body> </html>

childr.html为我们的子级页面,它嵌套在当前html当中。

在iframe中,当父级页面和子级页面的子域不同时,通过设置document.domain='xx'来将xx定为基础域,从而实现跨域。

postMessage

当两个页面不再同一个域时,我们可以通过postMessage来实现数据传输。(在iframe中

a.html代码:

 

html

复制代码

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <h2>a.html</h2> <iframe src="http://127.0.0.1:5500/postMessage/b.html" frameborder="0" id="iframe"></iframe> <script> // 给b发送数据 let iframe = document.getElementById('iframe') iframe.onload = function(){ let data = { name: 'Tom' } iframe.contentWindow.postMessage(JSON.stringify(data), 'http://127.0.0.1:5500') } // 监听b传来的数据 window.addEventListener('message', function(e){ console.log(e.data); }) </script> </body> </html>

b.html代码:

 

html

复制代码

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <h4>b.html</h2> <script> window.addEventListener('message', function(e){ console.log(JSON.parse(e.data)); if(e.data){ window.parent.postMessage('我接受到', 'http://127.0.0.1:5500') } }) </script> </body> </html>

大家可以试着打印一下,看看是否拿到了数据。

总结

JSONP、Cors、node代理、nginx代理这四种方法是我们常见的去解决跨域的手段,而这四种方法已经可以满足我们大部分的开发需求了。在面试过程中,大家主要去跟面试官说这四种方法,剩下的方法小伙伴们如果想到了也可以跟面试官讲。

写文章不易,如果帮助到了小伙伴们,可以给本文点赞收藏评论三连呀。有不懂的地方欢迎到评论区留言,我会及时回复。

  • 8
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值