一文解决面试中的跨域问题

前言

在前端面试中,跨域问题是十分常见的一个问题,很多人在面试时往往不能答得完整,导致不能达到面试官的预期 ,下面这篇文章 全面地讲讲 跨域问题吧!

什么是同源政策?

在讲跨域前,我觉得有必要跟面试官阐述下 什么是同源政策:

同源政策是一个安全机制,限制不同源(协议、域名、端口任意不同就认为是不同源)网页,阻止其读取彼此Cookie、DOM等敏感数据,防恶意网站越权访问。

对于第一次接触这个概念的小伙伴 听起来可能有点抽象,举个例子吧!

假设你正在使用银行网站(https://www.bank.com),且已登录(此时浏览器保存了银行种下的登录 Cookie)。若你不小心点开一个恶意链接(https://attack.com),这个恶意网站会尝试偷偷获取你浏览器中银行网站的 Cookie,再伪装成你向银行网站(https://www.bank.com)发起转账请求。!

如果没有同源政策的话 上面的操作就成功了 这种恶意网站 会诱使你点进,它不仅能够完成转账等操作,还能获取你的已登录的账户的一些私密信息等等 后果无法想象!

但有了同源政策后:由于恶意链接的源(https://attack.com)与银行网站的源(https://www.bank.com),域名明显不同,属于 "不同源",浏览器会直接触发同源政策拦截 ------ 恶意网站既无法读取银行的 Cookie、DOM 数据,也不能向银行发起伪装请求,相当于给你的账户加了一道安全锁。

小小的比喻

恶意链接 (源:https://attack.com):浏览器大哥,我是用户,想给指定账户转点钱,让我发个 POST 请求呗!

浏览器 :先查你的 "身份"------ 你来自https://attack.com,要操作的目标是https://www.bank.com/Transfer。协议虽都是 HTTPS,但域名一个是attack.com、一个是bank.com,明显不同源!想越权操作?门都没有!https://www.bank.com的信息你碰不到,请求也别想发!

银行真正的用户 (源:https://www.bank.com/account):浏览器大哥,我要给自己账户操作,转钱给家里人,通个行呗!

浏览器 :核对一下 ------ 你来自https://www.bank.com/account,目标是https://www.bank.com/Transfer,协议(HTTPS)、域名(bank.com)、端口(默认 443)全一致,符合同源要求!这关过了,你正常发请求操作就行~

什么是跨域

理解了同源政策后,"跨域" 就很好懂了 ------ 它本质是浏览器执行同源政策时,出现的 "源不匹配" 场景:当一个网页(A 源)要向另一个不同源的网页(B 源)发起请求(比如获取数据、提交表单),或访问其敏感资源(如 Cookie、DOM)时,就会触发 "跨域"。

1. 先明确:哪些情况算跨域?

判断标准和 "同源" 完全对应 ------ 只要两个源的协议、域名、端口三者中任意一个不同,从 A 源到 B 源的操作就属于跨域。举几个典型例子更直观:

发起请求的源(A) 目标源(B) 是否跨域? 原因
https://www.test.com http://www.test.com 协议不同(HTTPS vs HTTP)
https://www.test.com https://www.abc.com 域名不同(test.com vs abc.com
https://www.test.com:8080 https://www.test.com:8081 端口不同(8080 vs 8081)
https://www.test.com https://blog.test.com 主域名相同、子域名不同(www vs blog)
https://www.test.com https://www.test.com/info 协议、域名、端口完全一致(路径不同不影响同源)

2. 跨域≠"完全不能访问",要区分 "拦截场景"

很多人会误以为 "跨域 = 请求发不出去",但实际是:

  • 请求能发出去:比如 A 源向 B 源发 GET/POST 请求,网络层面是能到达 B 服务器的,B 也会返回响应;
  • 浏览器会拦截响应:关键在 "响应回来后"------ 浏览器发现这是 "跨域请求的响应",且 B 源没明确允许 A 源访问,就会触发同源政策,把响应拦截掉(开发者工具里能看到请求成功,但代码拿不到数据)。

这一点要注意:跨域的核心是 "浏览器端的拦截保护",而非 "禁止发起请求"。

3. 为什么会有跨域需求?

既然同源政策是安全机制,为啥还要跨域?因为实际开发中,"不同源协作" 很常见:

  • 比如前端项目部署在https://front.test.com,后端接口部署在https://api.test.com(子域名不同),前端要调用后端接口拿数据,就需要跨域;
  • 再比如网站要嵌入第三方地图(如高德地图 API,域名是https://restapi.amap.com)、拉取第三方天气数据,本质都是跨域请求。

正因为有这些合理需求,后来才出现了 "跨域解决方案"(如 CORS、JSONP 等)------ 既满足业务需求,又不突破同源政策的安全底线。

如何解决跨域?

接下来详细讲讲如何解决跨域

一、CORS(跨域资源共享):最推荐的主流方案

CORS(Cross-Origin Resource Sharing)是浏览器官方支持的标准方案,本质是 "后端明确告诉浏览器:允许哪个前端源访问我",无需前端额外复杂操作,是目前前后端分离项目的首选。

核心原理

  1. 前端发起跨域请求时,浏览器会自动在请求头里加 Origin 字段(比如 Origin: https://front.test.com),告诉后端 "我来自哪个源";
  2. 后端收到请求后,在响应头里加 允许跨域的字段 (关键是 Access-Control-Allow-Origin),比如 Access-Control-Allow-Origin: https://front.test.com(明确允许该前端源访问);
  3. 浏览器收到响应后,检查响应头的允许字段:若当前前端源在允许列表里,就不拦截响应,前端正常拿到数据;否则拦截。

听起来就像 后端 给可以访问的前端列表配置了 一个白名单 只有出现在白名单里面的 源 才能允许对该后端

关键响应头(后端配置)

响应头字段 作用 示例
Access-Control-Allow-Origin 核心!指定允许的前端源(* 表示允许所有源,生产环境不推荐) https://front.test.com*
Access-Control-Allow-Methods 允许的请求方法(GET/POST/PUT/DELETE 等) GET,POST,PUT
Access-Control-Allow-Credentials 是否允许前端带 Cookie(比如登录态),需前后端配合开启 true(开启后,Allow-Origin 不能是*

适用场景

  • 前后端分离项目(如 Vue/React 前端 + Java/Node.js 后端);
  • 能修改后端代码(需后端配置响应头),且浏览器支持(现代浏览器均支持,IE10+)。

二、Proxy 代理:前端本地开发常用方案

Proxy 代理的核心思路是:利用 "服务器端不受同源政策限制" 的特点,让前端请求先发给 "本地代理服务器",再由代理服务器转发给目标跨域接口(比如后端接口),从而绕开浏览器的跨域拦截。

核心流程(以 Vue 项目为例)

  1. 前端本地开发时,启动一个 "本地代理服务器"(比如 Vue CLI 自带的 webpack-dev-server);
  2. 前端原本要直接请求 https://api.test.com/getData(跨域),现在改成请求 本地代理地址 ,比如 http://localhost:8080/api/getData(和前端本地服务同源,无跨域);
  3. 本地代理服务器收到请求后,转发 给目标接口 https://api.test.com/getData(服务器端转发不受同源限制);
  4. 代理服务器拿到目标接口的响应后,再转发给前端,前端正常接收数据。

配置示例(Vue.config.js)

javascript 复制代码
module.exports = {
  devServer: {
    proxy: {
      '/api': { // 匹配所有以"/api"开头的请求
        target: 'https://api.test.com', // 目标跨域接口地址
        changeOrigin: true, // 开启代理:让后端认为请求来自代理服务器(而非前端)
        pathRewrite: { '^/api': '' } // 路径重写:去掉"/api"前缀(比如前端发"/api/getData",实际转发为"/getData")
      }
    }
  }
}

适用场景

  • 本地开发阶段(生产环境不适用,因为生产环境前端是静态资源,没有本地代理服务器);
  • 后端暂时无法配置 CORS,或需要快速绕开跨域调试接口。

三、JSONP:兼容老浏览器的备选方案

JSONP 是早期解决跨域的方案 ,利用 "<script>标签加载资源不受同源政策限制" 的特性实现(比如我们在页面里引入第三方 CDN 的 JS 文件,本质就是跨域加载)。

核心原理

  1. 前端定义一个 "回调函数"(比如 handleData),用于接收跨域接口返回的数据;
  2. 前端动态创建一个 <script> 标签,其 src 指向目标跨域接口,并在 URL 后拼接 "回调函数名",比如 https://api.test.com/getData?callback=handleData
  3. 后端收到请求后,按 "回调函数名 (数据)" 的格式返回数据(比如 handleData({ "name": "test", "age": 20 }));
  4. 浏览器加载 <script> 标签时,会执行返回的 "回调函数调用代码",前端的 handleData 函数就会被触发,拿到后端传的数据。

前端代码示例

javascript 复制代码
// 1. 定义回调函数
function handleData(data) {
  console.log('跨域拿到的数据:', data); // 比如拿到 { "name": "test" }
}

// 2. 动态创建script标签,发起JSONP请求
const script = document.createElement('script');
script.src = 'https://api.test.com/getData?callback=handleData'; // 拼接回调函数名
document.body.appendChild(script);

// 3. 请求完成后移除script标签(可选,清理DOM)
script.onload = () => {
  document.body.removeChild(script);
};

优缺点及适用场景

  • 优点:兼容所有浏览器(包括 IE 低版本);
  • 缺点 :仅支持 GET 请求 (因为 URL 有长度限制,且 POST 请求无法通过<script>标签发起),安全性较低(可能遭受 XSS 攻击);
  • 适用场景:需兼容 IE8 及以下的老项目,或调用仅支持 JSONP 的第三方接口(如早期的某些地图 API)。

四、Nginx 反向代理:生产环境的服务器端方案

如果项目已部署到生产环境(前端静态资源 + 后端接口分离部署),可通过 Nginx 反向代理 解决跨域 ------ 和 "本地 Proxy 代理" 原理类似,都是 "用服务器转发请求绕开浏览器限制",但作用于生产环境的服务器。

核心思路

  1. 生产环境中,前端静态资源(Vue/React 打包后的文件)和 Nginx 部署在同一台服务器,用户访问前端的地址是 https://www.test.com(Nginx 提供的地址);
  2. 前端要调用的跨域接口是 https://api.test.com/getData,现在改成请求 Nginx 的同一域名接口 ,比如 https://www.test.com/api/getData(和前端同源,无跨域);
  3. Nginx 收到 /api/getData 的请求后,通过配置 "反向代理规则",将请求转发给实际的后端接口 https://api.test.com/getData
  4. Nginx 拿到后端响应后,再转发给前端,完成跨域通信。

Nginx 配置示例(nginx.conf)

nginx 复制代码
server {
  listen 80;
  server_name www.test.com; # 前端访问的域名

  # 处理前端静态资源(比如Vue打包后的dist目录)
  location / {
    root /usr/local/nginx/html/front; # 前端静态文件路径
    index index.html;
  }

  # 反向代理:处理/api开头的请求,转发到后端接口
  location /api/ {
    proxy_pass https://api.test.com/; # 目标后端接口地址(末尾的/要加,否则路径会拼接错误)
    proxy_set_header Host $host; # 传递请求头,让后端知道原始请求的域名
  }
}

适用场景

  • 生产环境:前端和后端接口部署在不同域名,且无法修改后端配置(或不想让后端暴露真实地址);
  • 需统一入口域名(比如用户只访问 www.test.com,无需知道后端接口域名 api.test.com)。

总结:方案怎么选?

方案 核心优势 适用场景
CORS 标准、安全、支持所有请求方法 前后端分离项目(能改后端代码),首选
Proxy 代理 前端本地调试用,无需改后端 本地开发阶段(如 Vue/React 项目调试)
JSONP 兼容老浏览器 需兼容 IE8-,或调用仅支持 JSONP 的老接口
Nginx 反向代理 生产环境用,统一域名入口 生产环境部署,前端后端不同域名且需隐藏后端地址

实际开发中,90% 以上的场景优先用 CORS;本地调试搭配 Proxy 代理,生产环境复杂场景用 Nginx,JSONP 仅作为老项目兼容方案。

拓展:WebSocket和postMessage

既然聊到了 同源政策和跨域 那么下面会介绍下Websocketiframe 的 postMessage

要理解 Websocketiframe 的 postMessage,核心要先明确:它们都是绕开 "同源政策限制"、实现不同源页面 / 客户端间通信的技术,但适用场景、原理和用法完全不同。下面分两部分详细拆解:

五、Websocket:突破 HTTP 单向性,实现 "全双工跨域通信"

首先要区分一个前提:Websocket 本身不是为 "解决跨域" 设计的,但它天然支持跨域 ------ 因为它的通信协议(ws:// 或加密的 wss://)不遵循 HTTP 的同源限制,从根源上避开了跨域问题,更核心的价值是实现 "客户端与服务器的全双工实时通信"。

1. 为什么需要 Websocket?(先理解 HTTP 的痛点)

传统的 HTTP 通信是 "单向的、无状态的":

  • 只能由 客户端主动发请求,服务器被动响应;
  • 每次通信都要重新建立连接(HTTP/1.1 虽有长连接,但仍需客户端触发);
  • 若要实现 "实时更新"(如聊天、股票行情、直播弹幕),只能用 "轮询"(客户端每隔几秒发一次请求)或 "长轮询"(服务器 hold 住请求直到有数据),效率低、耗资源。

Websocket 就是为解决 "实时通信" 而生,同时天然支持跨域。

2. 核心原理:一次握手,持久连接,全双工通信

Websocket 的通信流程分两步,完全绕开同源限制:

  1. HTTP 握手(建立连接) :客户端先发送一个 特殊的 HTTP 请求 (请求头里带 Upgrade: websocketConnection: Upgrade),向服务器申请 "升级协议";服务器同意后,返回 101 Switching Protocols 响应,此时通信协议从 HTTP 升级为 Websocket,连接正式建立。✅ 关键:这一步的 HTTP 握手允许跨域(服务器无需额外配置 CORS,因为后续通信走 Websocket 协议)。
  2. 全双工通信(数据传输) :连接建立后,客户端和服务器可以同时双向发数据(类似打电话,你说的同时对方也能说),无需每次重新连接;数据格式更轻量(二进制或文本),延迟极低,适合实时场景。

3. 跨域通信示例(前端 + 后端)

以 "跨域聊天" 为例:客户端是 https://client.com(前端),服务器是 wss://server.com:8080(后端,不同源)。

① 前端(客户端)代码(浏览器原生支持 Websocket API):

javascript 复制代码
// 1. 建立跨域 Websocket 连接(直接连接不同源的服务器,无跨域报错)
const ws = new WebSocket('wss://server.com:8080/chat'); // 协议wss(加密),域名server.com(与客户端不同源)

// 2. 连接成功时触发
ws.onopen = () => {
  console.log('Websocket 跨域连接成功!');
  // 给服务器发消息(跨域发送,无需额外配置)
  ws.send('我是来自 client.com 的用户,请求加入聊天!');
};

// 3. 接收服务器发来的消息(实时推送)
ws.onmessage = (event) => {
  console.log('收到服务器消息:', event.data); // 比如收到其他用户的聊天内容
};

// 4. 连接关闭时触发
ws.onclose = () => {
  console.log('Websocket 连接关闭');
};

② 后端(服务器)代码 (以 Node.js 的 ws 库为例):

javascript 复制代码
const WebSocket = require('ws');
// 创建 Websocket 服务器,监听 8080 端口
const wss = new WebSocket.Server({ port: 8080 });

// 监听客户端连接
wss.on('connection', (ws) => {
  console.log('有客户端跨域连接进来了!');
  
  // 接收客户端消息
  ws.on('message', (message) => {
    console.log('收到客户端消息:', message.toString());
    // 给客户端回消息(跨域响应)
    ws.send(`服务器已收到:${message.toString()}`);
  });
});

4. 关键特点 & 适用场景

特点 说明
天然跨域 无需配置 CORS,直接连接不同源服务器
全双工 客户端 / 服务器可同时发数据,实时性极强
持久连接 一次握手后保持连接,减少连接开销
轻量数据 数据帧格式简单,比 HTTP 更省带宽

适用场景:实时聊天、直播弹幕、股票 / 期货行情更新、协同编辑(如在线文档)、物联网设备实时数据传输等。

六、iframe 的 postMessage:解决 "两个不同源页面间的双向通信"

iframe 是 HTML 标签,用于在一个页面中嵌入另一个页面(如在 https://a.com 中嵌入 https://b.com 的页面)。但由于同源政策,父页面和嵌入的子页面(iframe 内容)默认无法通信(比如父页面不能修改子页面的 DOM,子页面不能读取父页面的 Cookie)。

postMessage 是浏览器提供的 API,专门用于打破这种限制,实现 "不同源的父页面 ↔ 子页面(iframe)" 双向通信。

1. 核心原理:"定向发送 + 验证接收",避免恶意通信

postMessage 的设计很安全:发送方需要指定 "接收方的域名",接收方需要验证 "发送方的域名",防止恶意页面伪造消息。

  • 发送方 :通过 window.postMessage(data, targetOrigin) 发消息,targetOrigin 明确指定 "允许接收的域名"(如 https://b.com),只有该域名的页面能接收;
  • 接收方 :通过监听 window.onmessage 事件接收消息,接收后必须验证 "发送方的域名"(event.origin),确认是可信来源后再处理,避免恶意消息。

2. 通信场景:父页面(a.com) ↔ 子页面(iframe 嵌入的 b.com

假设场景:

  • 父页面:https://a.com/index.html(嵌入了 iframe);
  • 子页面:https://b.com/iframe-page.html(被 iframe 嵌入,与父页面不同源);
  • 需求:父页面给子页面传 "用户 ID",子页面接收后给父页面回传 "处理结果"。
① 父页面(a.com/index.html)代码:发送消息 + 接收子页面回复
html 复制代码
<!-- 父页面:嵌入不同源的子页面(iframe) -->
<iframe 
  id="myIframe" 
  src="https://b.com/iframe-page.html"  <!-- 子页面域名b.com,与父页面a.com不同源 -->
  width="500" 
  height="300"
></iframe>

<script>
// 1. 等待 iframe 加载完成(确保子页面已就绪)
const iframe = document.getElementById('myIframe');
iframe.onload = () => {
  // 2. 给子页面发消息(跨域发送)
  // 参数1:要发送的数据(可以是字符串、对象等,会自动序列化)
  // 参数2:targetOrigin - 允许接收的域名(* 表示任意域名,生产环境不推荐,需指定具体域名如https://b.com)
  iframe.contentWindow.postMessage(
    { type: 'SEND_USER_ID', userId: '123456' },  // 发送用户ID
    'https://b.com'  // 明确指定子页面域名,确保安全
  );
};

// 3. 监听子页面发来的回复(跨域接收)
window.addEventListener('message', (event) => {
  // 🔴 关键:验证发送方的域名(必须是可信的b.com,防止恶意页面伪造消息)
  if (event.origin !== 'https://b.com') {
    return; // 不是可信来源,直接忽略
  }

  // 处理子页面的消息
  console.log('父页面收到子页面回复:', event.data);
  // 比如子页面回传 { type: 'USER_ID_RECEIVED', status: 'success' }
  if (event.data.type === 'USER_ID_RECEIVED' && event.data.status === 'success') {
    alert('子页面已成功接收用户ID!');
  }
});
</script>
② 子页面(b.com/iframe-page...)代码:接收父页面消息 + 回复
html 复制代码
<!-- 子页面:被a.com的iframe嵌入 -->
<h1>我是b.com的子页面</h1>

<script>
// 1. 监听父页面发来的消息(跨域接收)
window.addEventListener('message', (event) => {
  // 🔴 关键:验证发送方的域名(必须是可信的a.com,防止恶意页面伪造消息)
  if (event.origin !== 'https://a.com') {
    return; // 不是可信来源,忽略
  }

  // 处理父页面的消息
  console.log('子页面收到父页面消息:', event.data);
  // 比如父页面发来 { type: 'SEND_USER_ID', userId: '123456' }
  if (event.data.type === 'SEND_USER_ID') {
    const userId = event.data.userId;
    console.log('父页面传的用户ID:', userId);

    // 2. 给父页面回传处理结果(跨域回复)
    // event.source 是"发送方的window对象"(即父页面的window)
    // event.origin 是"发送方的域名"(即https://a.com),直接用这个确保targetOrigin正确
    event.source.postMessage(
      { type: 'USER_ID_RECEIVED', status: 'success', msg: `已接收用户ID: ${userId}` },
      event.origin  // 直接用发送方的域名,避免写错
    );
  }
});
</script>

总结

跨域是前端面试与开发中的高频重点,其本质是浏览器同源政策下 "源不匹配" 的安全限制场景。要理解跨域,需先掌握同源政策 ------ 这一浏览器安全机制通过校验 "协议、域名、端口" 是否完全一致,限制不同源网页读取 Cookie、DOM 等敏感数据,避免恶意网站伪造操作(如伪装用户发起转账),是保障网页安全的核心防线。

当网页因协议、域名、端口任意一项不同,需向其他源发起请求或访问资源时,便会触发跨域。需注意,跨域并非 "请求发不出去",而是请求正常发送且后端返回响应后,浏览器因未收到目标源的允许指令,拦截了响应数据。实际开发中,前后端分离调用接口、集成第三方服务(如地图 API)等合理需求,催生了多种跨域解决方案。

主流方案各有适配场景:CORS 作为官方标准,通过后端配置响应头(如Access-Control-Allow-Origin)授权前端访问,是前后端分离项目的首选;Proxy 代理利用服务器端不受同源限制的特性,适合本地开发调试(如 Vue 项目配置devServer.proxy);JSONP 依托<script>标签跨域加载特性,仅支持 GET 请求,多用于兼容 IE 低版本或老接口;Nginx 反向代理则适用于生产环境,通过统一域名转发请求,隐藏后端真实地址并解决跨域。

此外,Websocket 因采用ws/wss协议,天然支持跨域,是实时通信(如聊天、弹幕)的最优选择;iframe 的postMessageAPI 则专门解决不同源父子页面的双向通信,通过 "定向发送 + 域名验证" 保障安全。

综上,跨域问题的解决需结合场景选型:90% 以上场景优先用 CORS,本地调试配 Proxy,生产环境复杂需求用 Nginx,特殊实时通信或页面交互则分别采用 Websocket 与postMessage,既满足业务需求,又不突破同源政策的安全底线。

相关推荐
阿白19552 小时前
JS基础知识——创建角色扮演游戏
前端
傻梦兽2 小时前
用 scheduler.yield() 让你的网页应用飞起来⚡
前端·javascript
然我2 小时前
搞定异步任务依赖:Promise.all 与拓扑排序的妙用
前端·javascript·算法
Focusbe2 小时前
为什么 “大前端” 需要 “微前端”?
前端·后端·架构
usagisah2 小时前
为 CSS-IN-JS 正个名,被潮流抛弃并不代表无用,与原子类相比仍有一战之力
前端·javascript·css
阿笑带你学前端2 小时前
Flutter应用自动更新系统:生产环境的挑战与解决方案
前端·flutter
不一样的少年_2 小时前
老板催:官网打不开!我用这套流程 6 分钟搞定
前端·程序员·浏览器
徐小夕2 小时前
支持1000+用户同时在线的AI多人协同文档JitWord,深度剖析
前端·vue.js·算法
小公主3 小时前
面试必问:跨域问题的原理与解决方案
前端