面试官:聊一聊跨域

331 阅读6分钟

当面试官让我们聊一聊某些东西时,我们首先要想到对话的方式,一般都遵循

  • xxx是什么?
  • xxx特性 (优缺点)?
  • xxx的应用场景?

这三点来进行回答,那么接下来我们来聊一聊跨域

跨域是什么?

跨域问题主要出现在前端开发中,特别是涉及到前后端分离的项目。当一个网页的JavaScript代码试图通过Ajax等方式访问不同源(包括协议、域名、端口号之一不同)的API接口时,就会遇到跨域问题,这就是浏览器的同源策略。浏览器为了安全性考虑,默认阻止了这种跨域请求,除非服务器明确允许。

一、同源策略:跨域问题的根源

什么是同源策略?

同源策略(Same-Origin Policy)要求以下三要素完全一致:

  • 协议(如 HTTP/HTTPS)
  • 域名(如 example.com
  • 端口(如 80/443)

同源策略(Same-Origin Policy)是一种重要的安全机制,用于控制不同源(origin)之间的交互。这里的“源”由协议、域名和端口号三部分组成。当两个URL的这三个部分完全相同时,则它们属于同一源;只要有任何一部分不同,它们就被认为是不同源。

  • http://www.example.com:80/a.html 与 https://www.example.com/b.html 不同源(协议不同)
  • http://shop.example.com 与 http://pay.example.com 不同源(子域名不同)

## 跨域的特性与矛盾

安全性保障

  • 数据安全:防止恶意网站窃取用户敏感数据
  • 服务器防护:减少 XSS、CSRF 等攻击风险
  • 资源隔离:避免第三方脚本随意操作 DOM

开发痛点

  • 前后端分离架构下,本地开发常需处理跨域
  • 第三方 API 集成时需协调跨域策略
  • 多子域系统(如微服务)需跨域通信

解决跨域问题的七种方案

一. jsonp

这是一个比较取巧的方法,利用script 标签src属性不受同源策略限制的特性,来发送请求

实现原理:

利用 <script> 标签不受同源策略限制的特性:

  • 前端携带一个参数 callback 给到后端
  • 后端将数据作为 callback 函数的实参,返回给前端一个 callback 的调用形式
  • 浏览器接收到callback的调用会自动执行全局的callback函数

特性:

  • 必须要前后端配合
  • 只能发送 get 请求
  • 不安全,容易受到xss攻击

代码示例:

前端
<!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 onclick="handle()">请求</button>
  <script>
    function jsonp(url, cb) {
      return new Promise((resolve, reject) => {

        const script = document.createElement('script')

        window[cb] = function (data) {
          // console.log(data) // 后端返回的数据
          resolve(data)
        }

        script.src = `${url}?cb=${cb}`

        document.body.appendChild(script)
        // callback('hello world')

      })
    }




    function handle() {
      jsonp('http://localhost:3000', 'callback').then(res => {
        console.log(res)
      })
    }
  </script>
</body>

</html>
后端
const http = require('http');

http.createServer((req, res) => {
  const query = new URL(req.url, `http://${req.headers.host}`).searchParams
  // console.log(query.get('cb'));
  
  if (query.get('cb')) {
    const cb = query.get('cb')  // 'callback'
    const data = 'hello world'
    const result = `${cb}("${data}")`   // "callback('hello world')"
    res.end(result)
  }

  // res.end('hello world')

}).listen(3000);

二. CORS

这是我们个人写项目最常用的解决跨域问题的方案

实现原理

后端设置 Access-Control-Allow-Origin: '域名白名单',来通知浏览器哪些域名可以跨域访问

代码实例:

前端
<!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 onclick="handle()">请求</button>

    <script>
      function handle() {
        const xhr = new XMLHttpRequest();
        xhr.open("GET", "http://localhost:3000", true);
        xhr.send();
        xhr.readystatechange = function () {
          if (xhr.readyState === 4 && xhr.status === 200) {
            console.log(xhr.responseText);
          }
        };
      }
    </script>
  </body>
</html>
后端
const http = require('http')

const server = http.createServer((req, res) => {
    res.writeHead(200, { 
        'Access-Control-Allow-Origin': 'http://192.168.1.1:5500/',  // 允许所有域名跨域
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',  // 允许的请求方式
        'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With'  // 允许的请求头
    })
    
    res.end('hello world')
})

server.listen(3000)

三. nginx 反向代理

Nginx 反向代理是一种服务器配置技术,它允许一台服务器作为另一台服务器的中介,接收来自客户端的请求,并将这些请求转发给后端服务器。在企业项目中,一般用这种方式解决跨域问题。

实现原理

前端服务器和后端服务器不在同一个域名下,前端服务器通过 nginx 反向代理来访问后端服务器

image.png 如图所示

  1. 客户端请求:用户尝试访问某个网站或服务时,其请求首先到达Nginx反向代理服务器。
  2. 请求转发:Nginx根据配置规则,决定将这个请求转发到哪个后端服务器。
  3. 处理请求:后端服务器接收到请求后进行处理,并将响应返回给Nginx反向代理服务器。
  4. 响应客户端:Nginx接收到后端服务器的响应后,再将该响应返回给原始的客户端。

四. node 中间件代理

实现原理

前端服务器和后端服务器不在同一个域名下,前端服务器通过 node 中间件来访问后端服务器

与nginx 反向代理大差不差,只是换了一种语言

五. websocket

传统的前后端通信是基于http协议的, 是单向的, 只能从一端发到另一端, 无法双向通信。为了解决这个问题,开发者编写了websocket方法

websocket的特点

  • websocket 是基于tcp协议的, 是双向的, 可以从一端发送到另一端, 也可以从另一端发送到一端
  • socket 协议一旦建立链接, 就可以一直保持通信状态, 不需要每次都建立链接
  • 天生就可以跨域

代码示例:

前端
<!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>
  <script>
    function WebSocketTest(url, params = {}) {
      return new Promise((resolve, reject) => {
          const socket = new WebSocket(url)
          socket.onopen = () => {
            socket.send(JSON.stringify(params))
          }
          socket.onmessage = (event) => {
            resolve(event.data)
          }
      })
    }


    WebSocketTest('ws://localhost:3000', {age: 18}).then(res => {
      console.log(res)
    })
  </script>
</body>
</html>
后端
const WebSocket = require('ws');

// 在 3000 端口上建立 WebSocket 伺服器 (随时都在线的服务)
const ws = new WebSocket.Server({ port: 3000 });

let count = 0

ws.on('connection', (obj) => {
  // console.log(obj);
  obj.on('message', (msg) => {  // 收到客户端发来的消息
    // console.log(msg.toString());
    obj.send('收到了')

    setInterval(() => {
      count++
      obj.send(count)
    }, 2000)
    
  })
})

六. postMessage

前面我们提到的跨域问题,都是产生在前端与后端传输数据时。其实,前端与前端也可以传输数据,并且也会有跨域问题,这里就要提到 iframe 标签了,它可以在一个前端页面中嵌套另一个页面,这两个页面进行数据传输时也会有跨域问题。

让我们用一个例子来说明:

父页面
<!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 id="frame" src="http://127.0.0.1:5500/%E8%B7%A8%E5%9F%9F/postMessage/detail.html" frameborder="0" width="800" height="500"></iframe>

  <script>
    let obj = {name: '阿杰', age: 18}

    document.getElementById('frame').onload = function () {
      this.contentWindow.postMessage(obj, 'http://127.0.0.1:5500') // 向iframe发送消息

      window.onmessage = function (e) { // 接收iframe发送的消息
        console.log(e.data);
      }

    }
  </script>
</body>
</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>
    <h3>详情页  --<span id="title"></span></h3>

    <script>
        let title = document.getElementById('title')

        window.onmessage = function(e){
            console.log(e.data);
            title.innerHTML = e.data.age

            e.source.postMessage('阿杰 20了', e.origin)  // 向父级页面发送信息
        }
        
    </script>
</body>
</html>

 通信流程示意图

父页面                      子页面
  |                            |
  |-- postMessage(data) ------>|
  |                            |
  |<----- responseMessage -----|
  |                            |

七. document.domain

document.domain 允许将页面的域名设置为当前域或其父域,从而实现跨子域通信。与上述方法一致,但谷歌已经禁用此方法

方法对比

方案适用场景优点缺点
JSONP兼容老旧浏览器无需服务端改动仅 GET,安全性低
CORS现代 Web 应用标准化,支持所有方法需服务端配合
Nginx 代理生产环境部署高性能,零侵入需运维知识
WebSocket实时通信场景全双工,低延迟协议升级成本高
postMessage跨窗口通信安全可控仅限窗口间通信

这些就是我们解决跨域问题的七种方法,看到这里,你对跨域问题应该有了更深的理解。

20200229174423_bzukt.jpg