前言
在前端面试中,跨域问题是十分常见的一个问题,很多人在面试时往往不能答得完整,导致不能达到面试官的预期 ,下面这篇文章 全面地讲讲 跨域问题吧!
什么是同源政策?
在讲跨域前,我觉得有必要跟面试官阐述下 什么是同源政策:
同源政策是一个安全机制,限制不同源(协议、域名、端口任意不同就认为是不同源)网页,阻止其读取彼此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)是浏览器官方支持的标准方案,本质是 "后端明确告诉浏览器:允许哪个前端源访问我",无需前端额外复杂操作,是目前前后端分离项目的首选。
核心原理
- 前端发起跨域请求时,浏览器会自动在请求头里加
Origin
字段(比如Origin: https://front.test.com
),告诉后端 "我来自哪个源"; - 后端收到请求后,在响应头里加 允许跨域的字段 (关键是
Access-Control-Allow-Origin
),比如Access-Control-Allow-Origin: https://front.test.com
(明确允许该前端源访问); - 浏览器收到响应后,检查响应头的允许字段:若当前前端源在允许列表里,就不拦截响应,前端正常拿到数据;否则拦截。
听起来就像 后端 给可以访问的前端列表配置了 一个白名单 只有出现在白名单里面的 源 才能允许对该后端
关键响应头(后端配置)
响应头字段 | 作用 | 示例 |
---|---|---|
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 项目为例)
- 前端本地开发时,启动一个 "本地代理服务器"(比如 Vue CLI 自带的 webpack-dev-server);
- 前端原本要直接请求
https://api.test.com/getData
(跨域),现在改成请求 本地代理地址 ,比如http://localhost:8080/api/getData
(和前端本地服务同源,无跨域); - 本地代理服务器收到请求后,转发 给目标接口
https://api.test.com/getData
(服务器端转发不受同源限制); - 代理服务器拿到目标接口的响应后,再转发给前端,前端正常接收数据。
配置示例(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 文件,本质就是跨域加载)。
核心原理
- 前端定义一个 "回调函数"(比如
handleData
),用于接收跨域接口返回的数据; - 前端动态创建一个
<script>
标签,其src
指向目标跨域接口,并在 URL 后拼接 "回调函数名",比如https://api.test.com/getData?callback=handleData
; - 后端收到请求后,按 "回调函数名 (数据)" 的格式返回数据(比如
handleData({ "name": "test", "age": 20 })
); - 浏览器加载
<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 代理" 原理类似,都是 "用服务器转发请求绕开浏览器限制",但作用于生产环境的服务器。
核心思路
- 生产环境中,前端静态资源(Vue/React 打包后的文件)和 Nginx 部署在同一台服务器,用户访问前端的地址是
https://www.test.com
(Nginx 提供的地址); - 前端要调用的跨域接口是
https://api.test.com/getData
,现在改成请求 Nginx 的同一域名接口 ,比如https://www.test.com/api/getData
(和前端同源,无跨域); - Nginx 收到
/api/getData
的请求后,通过配置 "反向代理规则",将请求转发给实际的后端接口https://api.test.com/getData
; - 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
既然聊到了 同源政策和跨域 那么下面会介绍下Websocket 和 iframe 的 postMessage。
要理解 Websocket 和 iframe 的 postMessage,核心要先明确:它们都是绕开 "同源政策限制"、实现不同源页面 / 客户端间通信的技术,但适用场景、原理和用法完全不同。下面分两部分详细拆解:
五、Websocket:突破 HTTP 单向性,实现 "全双工跨域通信"
首先要区分一个前提:Websocket 本身不是为 "解决跨域" 设计的,但它天然支持跨域 ------ 因为它的通信协议(ws://
或加密的 wss://
)不遵循 HTTP 的同源限制,从根源上避开了跨域问题,更核心的价值是实现 "客户端与服务器的全双工实时通信"。
1. 为什么需要 Websocket?(先理解 HTTP 的痛点)
传统的 HTTP 通信是 "单向的、无状态的":
- 只能由 客户端主动发请求,服务器被动响应;
- 每次通信都要重新建立连接(HTTP/1.1 虽有长连接,但仍需客户端触发);
- 若要实现 "实时更新"(如聊天、股票行情、直播弹幕),只能用 "轮询"(客户端每隔几秒发一次请求)或 "长轮询"(服务器 hold 住请求直到有数据),效率低、耗资源。
Websocket 就是为解决 "实时通信" 而生,同时天然支持跨域。
2. 核心原理:一次握手,持久连接,全双工通信
Websocket 的通信流程分两步,完全绕开同源限制:
- HTTP 握手(建立连接) :客户端先发送一个 特殊的 HTTP 请求 (请求头里带
Upgrade: websocket
和Connection: Upgrade
),向服务器申请 "升级协议";服务器同意后,返回101 Switching Protocols
响应,此时通信协议从 HTTP 升级为 Websocket,连接正式建立。✅ 关键:这一步的 HTTP 握手允许跨域(服务器无需额外配置 CORS,因为后续通信走 Websocket 协议)。 - 全双工通信(数据传输) :连接建立后,客户端和服务器可以同时双向发数据(类似打电话,你说的同时对方也能说),无需每次重新连接;数据格式更轻量(二进制或文本),延迟极低,适合实时场景。
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 的postMessage
API 则专门解决不同源父子页面的双向通信,通过 "定向发送 + 域名验证" 保障安全。
综上,跨域问题的解决需结合场景选型:90% 以上场景优先用 CORS,本地调试配 Proxy,生产环境复杂需求用 Nginx,特殊实时通信或页面交互则分别采用 Websocket 与postMessage
,既满足业务需求,又不突破同源政策的安全底线。