字节面试题:没有koa的cors你怎么处理跨域?

790 阅读10分钟

关于其他的字节面试文章已经更新得差不多了,希望能够帮助到各位

字节面试题:webpack的loader和plugin有什么区别? - 掘金 (juejin.cn)

字节面试题:为什么vite更快 - 掘金 (juejin.cn)

手撕深拷贝?这一篇就够了! - 掘金 (juejin.cn)

字节面试题:请你谈谈vue的响应式原理(一) - 掘金 (juejin.cn)

字节面试题:请你谈谈vue的响应式原理(二) - 掘金

字节面试题:没有koa的cors你怎么处理跨域? - 掘金

前言

“金九银十”的秋招,就像是落进池塘的羽毛,连点水花都没砸起来。已读不回是常态,能给面试已是情分,能拿offer的更是少之又少。可怜我在寒潮来临的深秋之际,也只得哀叹一句“越来越冷了啊...”

不过人总是要吃饭的,所以再怎么冷,卖炭翁也得卖炭,牛马攻城狮也得“攻城”,既是为了填饱肚子,也是为了“春天”做准备。所以今天为各位前端工程狮细聊一下前端面试中几乎必考的跨域

正文

同源策略

回想之前的字节面试,问到项目的时候,面试官亲切地问了一句:

你的项目中用到了koa,那你为什么要用koa这个框架呢?

主要是为了利用koa自带的cors去处理跨域

那你能讲讲cors处理跨域的原理吗?

(我已经慌了)这...这个不是很了解

(面试官扶额苦笑)那如果你不用koa,那么你怎么处理跨域呢

(我汗流浃背)呃...这个...可以用jsonp...

在聊跨域这个问题之前,我们先得搞清楚什么是同源策略。举个栗子,微信数据库存储了很多很多的微信用户的数据,那么当我们打开微信的时候,是会发送接口请求的,这很好理解。  

假设,有那么一天,我对我前女友的思念之情如滔滔江水一般连绵不绝,于是我想去看看她的朋友圈,看见她最近有没有和别的男生聊天。再假设,我无意之间在沙漠里捡到一张小纸条,上面又刚好写着微信聊天数据的后端接口。那我是不是就能看到我前女友是如何在深夜与他人倾诉对我的思念之情呢?  

很显然,用脚趾头都能想到这绝对不可能。而其中一道防火墙就是同源策略。同源策略是Web浏览器的一项重要安全机制,旨在防止一个网站的脚本非法访问另一个网站的数据。根据同源策略,只有当两个URL的协议、域名和端口号都相同时,它们才被认为是同源的。如果这三个条件中有任何一个不同,那么这两个URL就被认为是跨域的。  

跨域问题是指当一个请求从一个源发起,试图访问另一个源的资源时,如果这两个源不相同,浏览器会根据同源策略阻止这个请求。即使后端服务器会正常响应请求,但由于浏览器的同源策略限制,客户端无法接收到这些响应数据。也就是说,不管如何,只要发送了接口请求,后端就一定会响应,只不过数据到浏览器手里的时候被浏览器拦截了。所以同源策略发生在数据返回的过程中而不是请求发送的过程中。

1.png

这里我跑起来了两个js项目,一个前端vue,另一个后端的node。假设同源策略不存在,那么前端页面中就应该打印一个hello,但是很显然不行。再看看报错,果不其然被浏览器拦截了。  

<body>
    <script>
        fetch('http://localhost:3000', {
            method: 'GET',
        }).then(res => { return res.json() })
            .then(res => {
                console.log(res);

            })
    </script>
</body>

const http = require('http')

const server = http.createServer((req, res) => {
    res.writeHead(200, {
        'Content-Type': 'text',
        "accept-charset": "utf-8"
    })
    const data = {
        name: '彭于晏',
        age: 18
    }
    res.end(JSON.stringify(data))
})

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

2.png

跨域的解决方案

显然同源策略帮我们很大程度上解决了部分安全性的问题,但是对于前端而言似乎不那么友好。远的不说,就连我们自己写的前后端都拿不到数据,更何况是实际开发的大型项目呢?所以对于前端而言,掌握解决跨域问题是必不可少的

JSONP

JSONP(JSON with Padding)是一种解决跨域问题的古老方法,众所周知在html中可能会嵌入 script 标签,但是 script 可能会有src属性,既然有src就意味着会发送请求而 script 的src发送的请求不受同源策略的影响,所以jsonp主要利用了 script 标签不受同源策略限制的特点。其工作原理如下:

  1. 创建 script 标签:在客户端动态创建一个 script 标签,并将其 src 属性设置为目标URL。

  2. 定义回调函数:客户端定义一个全局函数,该函数用于处理返回的数据。

  3. 服务端响应:服务端将数据包裹在一个函数调用中返回,这个函数名就是客户端定义的回调函数名。

 代码demo如下

<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}`
                document.body.appendChild(script)
                //浏览器会添加一个script标签src为  "http://localhost:3000?cb=callback">

                window[cb] = (data) => {
                    console.log(data);
                }
            })
        }

        const btn = document.getElementById('btn')
        btn.addEventListener('click', () => {
            // 发送请求
            jsonp('http://localhost:3000', 'callback')
                .then(res => {
                    console.log(res);
                })
            // 后端返回一个"callback('hello world')"
            // 但是这个字符串被插入到script标签中,所以等效于
            //   <、script> callback("hello world") </、script>
            // 这也就是为什么返回字符串也会触发全局函数

        })
    </script>
</body>
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 = 'hello world'
    const str = `${cb}('${data}')`
    // str  = "callback('hello world')"
    ctx.body = str
}

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

需要解释一下的是后端用了一下koa框架让代码更简洁(这不重要),不过,后端返回的东西是一个字符串,值为“callback('hello world')”,可能很多人会好奇为什么字符串会变成代码去触发Window也就是全局身上的函数,实际上这里是把后端返回的字符串塞进了两个script标签字符串中,变成了一个更长的字符串

"<script> callback("hello world") </script>"

然后把字符串通过 document.body.appendChild(script)插入dom元素,就能成功触发全局身上的函数了。

CORS

CORS是一种现代的跨域解决方案,一般是后端通过在HTTP响应头中添加特定的字段来允许跨域请求。CORS的主要步骤如下:

  1. 预检请求:对于某些复杂的请求(如PUT、DELETE等),浏览器会先发送一个OPTIONS请求(预检请求),询问服务器是否允许该跨域请求。
  2. 实际请求:如果预检请求通过,浏览器会发送实际的请求。
  3. 响应头:服务器在响应头中添加以下字段来控制跨域行为:
  • Access-Control-Allow-Origin:指定允许访问该资源的外部域名。
  • Access-Control-Allow-Methods:指定允许的HTTP方法。
  • Access-Control-Allow-Headers:指定允许的HTTP头部字段。
  • Access-Control-Allow-Credentials:指定是否允许发送Cookie。

这里我们的后端就设置一个Access-Control-Allow-Origin用作演示  

<body>
    <button id="btn">请求按钮</button>

    <script>
        const btn = document.getElementById('btn')
        btn.addEventListener('click', () => {
            // 发送请求
            fetch('http://localhost:3000', {
                method: 'GET',

            }).then(res => { return res.json() })
                .then(res => { console.log(res) })
        })
    </script>
</body>
const http = require('http')

const server = http.createServer((req, res) => {
    // 跨域是浏览器不接受响应数据
    // 如果浏览器接受,那就不会跨域了
    res.writeHead(200, {
        // 'Access-Control-Allow-Origin': '*',//白名单
        'Access-Control-Allow-Origin': 'http://127.0.0.1:5500/',//白名单
        //允许来自http://127.0.0.1:5500/的请求,而其他的则会被拦截
    })
    const data = {
        name: '彭于晏'
    }
    res.end(JSON.stringify(data))
})

server.listen(3000, () => {
    console.log('3000 is running ');

})

Node.js 代理

1.png

根据刚刚这张图大家不难发现,跨域是因为同源策略,而同源策略又是浏览器干的。那能不能明知山有虎,咱就不去明知山呢?Node代理就是这个道理。前端与后端数据交互关我后端与后端数据交互什么事?还是来个前后端的demo,二者依旧是不同接口。

<template>
  <div>
    hello world
  </div>
</template>

<script setup>
import axios from 'axios'
import { onMounted } from 'vue'

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

<style scoped></style>

const http = require('http')

const server = http.createServer((req, res) => {
    // 跨域是浏览器不接受响应数据
    // 如果浏览器接受,那就不会跨域了

    const data = {
        name: 'hgjw'
    }
    res.end(JSON.stringify(data))
})

server.listen(3000, () => {
    console.log('3000 is running ');

})

前端是靠vite创建的,所以会有一个vite.config文件,在这个文件中做如下配置

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vite.dev/config/
export default defineConfig({
  plugins: [vue()],
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/api/, '')
      }
    }
  }
  // 如果现在尝试axios.get("/api")就会重定向到http://localhost:3000/api
  // 如果后面还有用api的话,就会被rewrite过滤成空字符串
  // 这里vite启动了一个新的后端服务,向http://localhost:3000发送请求
  // 但是后端之间发送请求不经过浏览器所以不受同源策略的限制
  // 通过代理解决同源策略问题
  // 但是打包之后vite失效了,所以只在开发环境生效
})

这样就可以绕开同源策略的监察,从而实现前后端数据交互,但是问题在于,vite只是手脚架,项目搭建完打包的时候可不会把手脚架打包进去,所以这种方法只能在开发环境中生效。

Nginx 反向代理

既然node代理只能在开发环境生效,那么生产环境呢?在生产环境中,可以使用Nginx作为反向代理服务器来解决跨域问题。通过配置Nginx,可以将客户端请求转发到目标服务器,并且在响应中添加必要的CORS头。这种方法原理类似cors配置白名单,是目前绝大多数公司使用的方法。不过原理涉及到Linux和数据库,这里就不做赘述了。

 

冷门方法

通常情况下,讲完上面四种基本也就差不多了,但是考虑到最近“越来越冷了”,还是拓展两种比较冷门的方法比较好。这里的所谓“冷门方法”主要都是用在iframe标签上,负责父子页面之间的通信。

Document.domain

在某些情况下,可以通过设置document.domain来实现子域之间的通信。这里有a,b两个页面,其中src地址要根据自己电脑实际跑起来的html项目去设置 a页面

<body>
    <h2>父级页面</h2>
    <iframe src="http://127.0.0.1:5500/domain/child.html" frameborder="0"></iframe>
   
   <script>
        document.domain = '127.0.0.1'
        var user = '彭于晏'
    </script>
</body>

b页面

<body>
    <h3>子界面</h3>
    <script>
        document.domain = '127.0.0.1'
        console.log(window.parent.user);
    </script>
</body>

  1.png

postMessage

postMessage 方法允许不同源的窗口之间进行安全的消息传递。这在嵌套的iframe场景中特别有用。 a页面

<body>
    <h2>aaa</h2>
    <iframe src="http://127.0.0.1:5500/postMseeage/b.html" frameborder="0" id="iframe"></iframe>

    <script>
        let iframe = document.getElementById('iframe')
        iframe.onload = function () {
            // 获取子页面的window对象
            let data = { name: '彭于晏' }

            setInterval(() => {
                iframe.contentWindow.postMessage(JSON.stringify(data), 'http://127.0.0.1:5500/postMseeage/b.html')
            }, 500);
        }

        window.addEventListener('message', function (e) {
            console.log(JSON.parse(e.data));
        })
    </script>
</body>

b页面

<body>
    <h4>bbbbbbbb</h4>
    <script>
        window.addEventListener('message', function (e) {
            console.log(JSON.parse(e.data));
            if (e.data) {
                window.parent.postMessage(JSON.stringify({ age: 157 }), 'http://127.0.0.1:5500/postMseeage/a.html')
            }
        })
    </script>
</body>

1.png postMessage原理类似vue路由之间的传值子父页面通过发送监听接收去进行数据的传输

总结

跨域问题是Web开发中常见的挑战,但通过多种技术手段可以有效解决。无论是古老的JSONP,还是现代的CORS,或是灵活的代理和消息传递机制,每种方法都有其适用场景和优缺点。了解这些技术背后的原理,可以帮助开发者更好地选择合适的方案,确保应用程序的安全性和功能性。希望本文能够帮助你更深入地理解跨域和同源策略,以及如何在实际项目中应对这些问题。最后祝各位读者姥爷0 waring(s),0 error(s)!