详谈跨域

这两天一直被面试官问到跨域,但是自己从来没有系统性地总结过,本期文章就来把跨域这个问题给聊明白来,也希望对春招的各位有所帮助

同源策略-跨域

跨域就是浏览器的同源策略生效的时候

后端返回给浏览器的数据会被浏览器的同源策略给拦截下来

就拿百度官网为🌰,https://www.baidu.com,其实这个地址是被处理了,理应是这样https//192.168.31.45:8080/user,正常来讲一个url地址是由四个部分组成,也就是协议号https,域名192.168.31.45,端口号8080以及路径user

我们可以正常写一个前端,假设那个地址就是百度的后端地址,我们可以从那里拿到百度的数据,我们现在想想,这个情景科学吗?显然不科学,数据怎么能被不认识的人随便拿!因此浏览器针对这个问题,里面有个同源策略,也就是协议号-域名-端口号三部分必须是一样的,浏览器才认为你们是一家公司的,但凡三者有一个不同,那么浏览器就会把这个请求拦截下来,此时就是跨域

比如这样的地址就是符合同源https//192.168.31.45:8080/userhttps//192.168.31.45:8080/list,只有最后的路径不同

也就可以这样理解,字节的前端朝着字节后端发接口请求,字节就会响应回去,然后百度前端也朝着字节后端发接口请求,字节后端会响应会去,后端是不负责判断谁来请求的,是个人请求都会返回,但是这个过程中,浏览器的同源就发现了不对,因此把后端返回的响应给拦截了下来

因此跨域发生在后端响应阶段

同源策略的目的就是一个安全性,怎么可能是个人都可以拿自己的数据

接下来我们需要解决跨域

回答这个之前我们先要明白为什么要解决跨域

为什么要解决跨域

假设我们在公司写项目,前端用vue写的,项目跑在http://192.168.31.1:8080,后端用go写的,跑在http://192.168.31.2:8080,尽管连接着一个wifi在同一个局域网内,其ip地址最后的数字还是不同的,两台设备的ip地址是不可能一样的,并且有时候,其端口号也是不同的

这是开发阶段,前后端需要联调,前端发现这个接口怎么都调用不了,问题就来了

所以为何要解决跨域,解决跨域方便程序员进行开发,开发阶段好调试

同源策略安全的同时,给程序员上了一层颈箍咒

解决跨域

解决跨域就是让同源策略发挥不了作用,后端响应回数据时可以正常作用

解决跨域有很多种方法,但是常用的就只有四种,这四种一定得掌握

JSONP

先简单实现下,前后端交互

go 复制代码
// 目录
client
	index.html
server // npm init -y
	node_modules
	app.js
	package-lock.json
	package.json

先把app.js写成这样,完全可以接受吧~(引用的koa

javascript 复制代码
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');
})

前端的话,简单写个页面,点击按钮获取后端返回的数据

index.html写成这样,我们也可以接受~

xml 复制代码
<!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发请求
			fetch('http://localhost:3000')
			.then(response => {
				return response.json() // fetch需要我们格式化数据
			})
			.then(res => {
				console.log(res)
			})
		})
    </script>
</body>
</html>

这样就实现了从后端拿数据,好,我们现在用liver server跑一下,点击按钮,出现报错!

has been blocked by CORS就是出现了跨域

当我们自己写全栈项目的时候,因为跨域导致自己的前端都无法调用自己的后端,感觉很气!

这个代码我改巴改巴,我把fetch请求注释掉,用scriptsrc来请求,你会发现,不再同源了

xml 复制代码
<!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发请求
			// fetch('http://localhost:3000')
			// .then(response => {
			// 	return response.json() // fetch需要我们格式化数据
			// })
			// .then(res => {
			// 	console.log(res)
			// })
		})
    </script>
</body>
</html>

嚯~居然不报错!

聪明的你这个时候就发现了,这不就是我们引入第三方源码的手段嘛,就那个CDN引入,引入那个资源也没有报错!

当我们使用ajax发接口请求的时候一定是会受同源的影响,但是我们通过scriptsrc去请求数据,并没有受到同源的影响

其实不法分子解决跨域就是通过这个手段

如果这个手段也受同源的影响,前端代码就写不了了,根本无法引入第三方的库

好,现在我们就钻这个空子

自己封装一个函数jsonp用于发接口请求,在函数里面自己生成一个script标签,这个函数可以接收urlcb参数,并且给script挂上一个src属性,放上urlcb。然后通过appendChildscript放到body里面去,这个时候就已经保证了这个函数可以利用script发请求了

xml 复制代码
<!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>

        function jsonp (url, cb) {
            return new Promise((resolve, reject) => {
                const script = document.createElement('script') // 可以创建h5的任意标签
                script.src = `${url}?cb=${cb}` // http://localhost:3000?cb='callback' 前端写死一个字符串传给后端 
                document.body.appendChild(script) // 把script添加到body中去,浏览器会自动发请求了
            })
        }

        let btn = document.getElementById('btn')
        btn.addEventListener('click', () => {
            jsonp('http://localhost:3000', 'callback')
            .then(res => {
                console.log('后端返回的结果:'+res);
            })
        })
    </script>
</body>
</html>

点击按钮,确实有个网络请求

既然前端发送了请求,那么此时后端一定收到了请求,并且里面把前端传进来的callback字符串传了过来

我们可以在后端的main方法中,打印下ctx.query

好,现在对后端代码改巴改巴,我把前端传给我的callback再带上自己数据返回给前端,如下,用的字符串模板拼接~

ini 复制代码
const Koa = require('koa')
const app = new Koa()

const main = (ctx, next) => {
    console.log(ctx.query);

    const data = '给前端的数据'
    const cb = ctx.query.cb // callback字符串
    const str = `${cb}('${data}')` // callback('给前端的数据')字符串
    
    ctx.body = str
}

app.use(main)
app.listen(3000, () => {
    console.log('listening on port 3000');
})

这个时候前端收到数据是报错的,因为cb还没有定义呢~

好,现在来到前端,我在全局window上挂上这个cb,其值我写成函数体

xml 复制代码
<!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>

        function jsonp (url, cb) {
            return new Promise((resolve, reject) => {
                const script = document.createElement('script') // 可以创建h5的任意标签
                script.src = `${url}?cb=${cb}` // http://localhost:3000?cb='callback' 前端写死一个字符串传给后端 
                document.body.appendChild(script) // 把script添加到body中去,浏览器会自动发请求了

                window[cb] = (data) => { // 把callback挂到window上去,然后值是一个函数体
                    console.log(data);
                }
            })
        }

        let btn = document.getElementById('btn')
        btn.addEventListener('click', () => {
            jsonp('http://localhost:3000', 'callback')
            .then(res => {
                console.log('后端返回的结果:'+res);
            })
        })
    </script>
</body>
</html>

我们打印在这个参数data,就是后端往cb括号中放入的数据,也就是那句话

既然有打印,说名这个cb函数被触发了,可是前端并没有触发它,那就只能是后端触发的,并且传了参数进来!

我现在将log打印改成resolve,这样后面的then就能打印出后端返回的数据了

xml 复制代码
<!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>

        function jsonp (url, cb) {
            return new Promise((resolve, reject) => {
                const script = document.createElement('script') // 可以创建h5的任意标签
                script.src = `${url}?cb=${cb}` // http://localhost:3000?cb='callback' 前端写死一个字符串传给后端 
                document.body.appendChild(script) // 把script添加到body中去,浏览器会自动发请求了

                window[cb] = (data) => { // 把callback挂到window上去,然后值是一个函数体
                    resolve(data);
                }
            })
        }

        let btn = document.getElementById('btn')
        btn.addEventListener('click', () => {
            jsonp('http://localhost:3000', 'callback')
            .then(res => {
                console.log('后端返回的结果:'+res);
            })
        })
    </script>
</body>
</html>

我们没有跨域,但是也拿到了后端返回的数据~

解释下整个过程:

  1. 借助scriptsrc属性给后端发一个请求,且携带一个参数callback
  2. 前端在window上添加了一个callback函数;
  3. 后端接收到这个参数callback后,将要返回给前端的数据data和这个参数callback进行拼接,成callback(data),并返回给前端;
  4. 因为window上已经有一个callback函数,后端又返回了一个形入callback(data),浏览器会将字符串执行成callback的调用

因此JSONP核心理念就是借助script标签上的src属性不受同源策略的影响这一机制,来实现跨域

总结

  1. ajax请求受到同源策略的影响,但是script上的src属性不受同源策略的影响,并且该属性也会导致浏览器发送一个请求
  2. 缺点:1. 必须后端配合(拿到参数再拼接回去);2. 只能用于get请求,浏览器执行scriptsrc请求默认就是get方式(正常开发很多接口都是post);

Cors

Cors(Cross-Origin Resource Sharing)

http协议中,每个请求都可以拆分成两部分,一个请求头,一个请求体

  • 请求头比较小,里面包含了这个请求的基本信息,从哪去哪儿
  • 请求体就放的是参数,数据包

后端返回的就是响应头和响应体,这个时候我们对这个响应头写入一些参数,告诉浏览器我后端的数据所有的前端请求都可以拿走

javascript 复制代码
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');
})

前端不需要任何变化

xml 复制代码
<!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')
            .then(res => res.json())
            .then(res => {
                console.log(res);
            })
        })
    </script>
</body>
</html>

好了,这样就实现了解决跨域,但是目前的写法比较偷懒,肯定不能写*,实际开发不可能允许所有的接口请求

比如我现在前端写在本地,就是localhost,所以把*换成http://127.0.0.1:5501

多个ip需要接口,就多配几个白名单

总结

后端通过设置响应头来告诉浏览器不要拒绝接收后端的响应

这个方法明显比JSONP好用多了,前端不需要任何操作,只需要后端简单配置下cors即可

node代理

假设我现在写了个前端,希望拿到网易云的数据,我可以从前端向网易云的后端拿,我还可以选择自己写个后端,自己的后端去往网易云的后端拿数据,这个过程中不经过浏览器,就不会跨域,后面就是自己的前端从自己的后端拿数据cor下就好,这就是node代理

vite这个构建工具就是用node写的,我们写vue项目的时候,就可以用node代理的形式去解决跨域问题

好,我现在用vite模仿下,App.vue我让其首页挂载完毕就朝着后端发请求

xml 复制代码
<script setup>
import axios from 'axios'
import { onMounted } from 'vue'

onMounted(() => {
  axios.get('http://localhost:3000')
  .then((res) => {
    console.log(res);
  })
})
</script>

这里用的axios发请求,axios需要自己安装npm i axios

然后自己的后端如下,没有使用cors,一定会发生跨域

javascript 复制代码
const http = require('http')

const server = http.createServer((req, res) => {
    let data = {
        msg: "hello nodo-proxy"
    }
    res.end(JSON.stringify(data)) // 向前端返回数据
})

server.listen(3000, () => {
    console.log('listening on port 3000');
})

配置vite.config.js

如何解决呢?我们直接来到vite.config.js文件中配置server,如下

javascript 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()], // vite源码是node写的 
  server: { // 和网络请求相关的配置
    proxy: {
      '/api': { // 只要前端是向/api发请求,都是发到target,比如axios.get('/api')
        target: 'http://localhost:3000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '') // 后端路径本身就有/api就把它去掉
      }
    }
  }
})

vite解决跨域:开发服务器选项 | Vite 官方中文文档 (vitejs.dev)

这个配置的意思是,只要前端朝/api发请求,那么就会转发到target中,也就是localhots:3000,然后改变源,如果后端路径本身就有/api,那么就重写为空

好了,所以现在只需要把前端的请求地址改成/api即可

xml 复制代码
<script setup>
import axios from 'axios'
import { onMounted } from 'vue'

onMounted(() => {
  axios.get('/api')
  .then((res) => {
    console.log(res);
  })
})
</script>

像是修改完配置文件,都需要项目重新启动下

好了,成功解决跨域,从后端拿到数据

好,问题来了,vite只是我们开发阶段使用的构建工具而已,项目最后是要打包上线的,因此到了生产阶段,vite打包后的这个配置信息是会消失的,不对,是整个vite的源码都会被剔除掉

因此这个方法只适用于开发环境,上线的跨域只能用别的方法

总结

vite帮我们启动了一个node服务,且帮我们朝着http://locahost:3000发请求,因为后端之间没有同源策略,所以,vite中的node服务能直接请求到数据,再提供给前端使用

但是给到前端依旧会跨域,只是因为里面已经自带cors了,看不出来

缺点:只能在开发环境中生效

截至目前,前端是没有一个优雅的手段可以阶段跨域的,JSONP很麻烦,而且只能get,然后Cors是后端干的,然后node只能开发阶段生效,其实这个三个方法通常都是用在开发环境下

nginx代理

这个机制和Cors差不多,做白名单的配置,都是配置请求头,需要后端在服务器上安装nginx,实现所有的请求可以实现nginx代理,nginxlinux的语法,而非js

这个方案可以解决生产环境下的跨域,也就是可以项目上线且不跨域,公司项目一般就是用这种方法解决跨域

nginxcors机制差不多,为何不用nginx呢?

如何你写了三个后端项目,那么三个后端项目都需要配置Corsnginx可以一起配置掉

具体实现以后再出期文章详聊,涉及到项目上线

其实这四种跨域方案足够你项目的开发了,不过面试官可能会问你还有吗,那些就是些不常用的方法

面试官:还有吗?

domain

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

iframe的作用就是一个html可以嵌套另一个html

比如我这里,两个页面,父级页面中通过iframe嵌套了个子页面,父级页面定义个参数,子级页面打印这个参数,当然得实现非同源的时候打印才能证明实现跨域,用live-server是同源运行的

live-server安装:npm i -g live-server

index.html

xml 复制代码
<!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="http://127.0.0.1:5501/%E9%9D%A2%E8%AF%95%E9%A2%98/%E7%99%BE%E5%BA%A6%E9%9D%A2%E8%AF%95%E9%A2%98/domain/child.html" frameborder="0"></iframe>

    <script>
        document.domain = '127.0.0.1'

        var user = 'admin'
    </script>
</body>
</html>

child.html

xml 复制代码
<!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>子页面</h4>

    <script>
        document.domain = '127.0.0.1'

        console.log(window.parent.user);
    </script>
</body>
</html>

比如当你希望在www.example.com中使用api.example.comapi,为防止跨域,就可以两个页面设置相同的document.domain,也就是基础域,这样就不会跨域

postMessage

实现一个深拷贝可以借助管道通信,也就是postMessage,还有个structured clone,这是js自带的

postMessage主要作用就是用来做管道通信的,既然涉及到通信,那就会遇到跨域的问题

按道理两个页面交互需要一个点击事件,这里我们不用点击事件,ab页面交互信息如下,不发生跨域

a.html

xml 复制代码
<!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:5501/%E9%9D%A2%E8%AF%95%E9%A2%98/%E7%99%BE%E5%BA%A6%E9%9D%A2%E8%AF%95%E9%A2%98/postMessage/b.html" frameborder="0" id="iframe"></iframe>

    <script>
        // 给b发送数据
        let iframe = document.getElementById('iframe')
        iframe.onload = function() {
            let data = {
                name: 'Dolphin'
            }
            iframe.contentWindow.postMessage(JSON.stringify(data), 'http://127.0.0.1:5501')  // a向b发送这个data数据
        }
        // 监听b传过来的数据
        window.addEventListener('message', function(e) {
            console.log(e.data);
        })
    </script>
</body>
</html>

b.html

xml 复制代码
<!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</h4>

    <script>
        window.addEventListener('message', function(e) {
            console.log(JSON.parse(e.data));  // 可以拿到a的数据

            if (e.data) { // 回应a,拿到数据
                setTimeout(function() {
                    window.parent.postMessage('我接受到了', 'http://127.0.0.1:5501')
                }, 2000)
            }
        })
    </script>
</body>
</html>

还有个websocket来解决跨域,以后再来单独详聊

最后

正常来讲,JSONPCorsnode代理nginx足够解决跨域了,如果面试官问你,还能答出个几个冷门方法就再合适不过了

如果你对春招感兴趣,可以加我的个人微信:Dolphin_Fung,我和我的小伙伴们有个面试群,可以进群讨论你面试过程中遇到的问题,我们一起解决

另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请"点赞+评论+收藏"一键三连,感谢支持!

相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax