在前后端分离架构成为主流的今天,跨域几乎是每个前端开发者绕不开的问题。浏览器的同源策略像一道 “安全墙”,保护着用户数据,却也给我们的开发带来了不少挑战。本文将从跨域的根源讲起,结合实战代码,系统拆解 7 种主流跨域方案的原理、实现、优缺点及适用场景,让你彻底搞懂跨域。
一、跨域的本质:同源策略是什么?
想要解决跨域问题,首先要明白 “跨域” 从何而来。
1. 同源策略的定义
浏览器的同源策略(Same-Origin Policy) 是跨域的核心根源,它是浏览器最核心也最基本的安全功能。所谓 “同源”,要求两个页面的:
- 协议(http/https)相同
- 域名(包括主域名、子域名)相同
- 端口号(80/443/3000 等)相同
只要三者有其一不同,就会被判定为 “跨域”,浏览器会限制非同源页面的以下行为:
- 读取非同源网页的 Cookie、LocalStorage、IndexedDB 等存储数据
- 获取非同源网页的 DOM 元素
- 向非同源地址发送 AJAX 请求(XMLHttpRequest/fetch)
2. 为什么需要同源策略?
试想一下,如果没有同源策略:
- 恶意网站可以轻易读取你网银页面的 Cookie,盗取账户信息
- 钓鱼网站可以嵌入真实的电商页面,篡改支付金额
- 任意网站都能向你的服务器发送伪造请求,发起 CSRF 攻击
同源策略就像一道 “防火墙”,从根本上限制了恶意网站的非法操作,保障了用户的信息安全。
3. 跨域的常见场景
日常开发中,跨域几乎无处不在:
- 前后端分离项目:前端运行在
localhost:5173,后端接口在localhost:3000(端口不同) - 调用第三方接口:如支付、地图、天气等第三方服务(域名不同)
- 多端协作:公司内部不同部门的系统对接(子域名不同)
接下来,我们进入正题,逐一拆解主流的跨域解决方案。
二、方案 1:JSONP—— 兼容性拉满的 “老古董”
JSONP(JSON with Padding)是跨域方案中的 “老前辈”,也是早期前端解决跨域最常用的方式,最大的优势是浏览器兼容性极好(甚至能兼容 IE6/7)。
1. JSONP 的核心原理
浏览器的同源策略限制了 AJAX 请求,但并没有限制 <script> 标签的 src 属性 ——<script> 可以加载任意域名的资源(比如 CDN 上的 jQuery)。JSONP 正是利用这一 “漏洞” 实现跨域。
简单来说:
- 前端动态创建
<script>标签,通过src向跨域接口发送请求,同时传递一个回调函数名 - 后端接收到请求后,将数据包裹在回调函数中返回(即 “JSON with Padding”)
- 前端的回调函数被执行,拿到跨域数据
2. JSONP 实战实现
前端代码(封装 JSONP 函数)
// 封装JSONP请求函数,返回Promise方便异步处理
function jsonp({ url, params, callback }) {
return new Promise((resolve, reject) => {
// 1. 创建script标签
let script = document.createElement('script')
// 2. 定义全局回调函数,接收后端返回的数据
window[callback] = function(data) {
resolve(data) // 成功拿到数据,resolve Promise
document.body.removeChild(script) // 移除script标签,避免污染
}
// 3. 拼接请求参数(包含回调函数名)
params = { ...params, callback } // 比如:{wd: 'test', callback: 'show'}
let arrs = []
for (let key in params) {
arrs.push(`${key}=${params[key]}`)
}
// 4. 设置script的src属性,发送请求
script.src = `${url}?${arrs.join('&')}`
document.body.appendChild(script)
// 5. 处理请求失败场景
script.onerror = function() {
reject(new Error('JSONP请求失败'))
document.body.removeChild(script)
}
})
}
// 调用JSONP请求
jsonp({
url: 'http://localhost:3000/say',
params: { wd: 'Iloveyou' },
callback: 'show'
}).then(data => {
console.log('JSONP请求结果:', data)
}).catch(err => {
console.error(err)
})
后端代码(Node.js 原生实现)
const http = require('http');
const server = http.createServer((req, res) => {
// 匹配/say接口
if (req.url.startsWith('/say')) {
// 解析URL参数
const url = new URL(req.url, `http://${req.headers.host}`);
const callback = url.searchParams.get('callback'); // 获取回调函数名
// 设置响应头:返回JS脚本
res.writeHead(200, { 'Content-type': 'text/javascript' });
// 构造返回数据,包裹在回调函数中
const data = {
id: 1,
username: 'admin',
msg: 'JSONP请求成功'
}
// 核心:返回 "回调函数(数据)" 格式的JS代码
res.end(`${callback}(${JSON.stringify(data)})`);
} else {
res.writeHead(404);
res.end('Not Found')
}
})
server.listen(3000, () => {
console.log('JSONP服务器运行在 http://localhost:3000');
})
3. JSONP 的优缺点
优点:
- 兼容性极强:支持所有主流浏览器,包括低版本 IE
- 实现简单:无需复杂的配置,前端后端少量代码即可完成
缺点:
- 仅支持 GET 请求:因为
<script>标签的src只能发起 GET 请求 - 安全风险:容易遭受 XSS 攻击(加载的脚本可能包含恶意代码),需确保请求的服务器是可信的
- 性能问题:额外加载的
<script>标签会阻塞页面渲染,影响首屏加载速度 - 无标准规范:不同服务器的实现方式可能不一致,兼容性需额外处理
4. 适用场景
仅推荐在兼容老旧浏览器的场景下使用,现代项目优先选择其他方案。
三、方案 2:CORS—— 现代跨域的 “标准答案”
CORS(Cross-Origin Resource Sharing,跨源资源共享)是 W3C 标准,也是目前解决跨域最主流、最推荐的方案。它通过在 HTTP 响应头中添加规则,让服务器明确告知浏览器:“哪些源可以访问我的资源”。
1. CORS 的核心原理
CORS 是一种基于 HTTP 头的机制,核心逻辑是:
- 浏览器发起跨域请求时,会自动在请求头中添加 Origin 字段(标识请求来源)
- 服务器根据 Origin 判断是否允许该源访问,通过响应头返回授权信息
- 浏览器根据服务器的响应头,决定是否允许前端获取响应数据
2. CORS 的两种请求类型
CORS 将跨域请求分为 “简单请求” 和 “复杂请求”,处理逻辑不同。
(1)简单请求
同时满足以下条件的请求为简单请求:
- 请求方法:GET、POST、HEAD
- 请求头仅包含:Accept、Accept-Language、Content-Language、Content-Type(仅限 application/x-www-form-urlencoded、multipart/form-data、text/plain)
处理逻辑:浏览器直接发送真实请求,服务器返回带 CORS 响应头的结果,无需额外步骤。
(2)复杂请求
满足以下任一条件即为复杂请求:
- 请求方法:PUT、DELETE、PATCH 等
- 请求头包含自定义字段(如 X-Custom-Header)
- Content-Type 为 application/json 等非简单类型
处理逻辑:浏览器会先发送预检请求(Preflight Request) (方法为 OPTIONS),询问服务器 “是否允许该跨域请求”;服务器同意后,浏览器才会发送真实请求。
3. CORS 核心响应头
服务器通过以下响应头控制跨域规则:
| 响应头 | 作用 |
|---|---|
| Access-Control-Allow-Origin | 允许访问的源(* 表示所有源,或指定具体域名如 http://localhost:5500) |
| Access-Control-Allow-Methods | 允许的请求方法(如 GET,POST,PUT,DELETE) |
| Access-Control-Allow-Headers | 允许的自定义请求头(如 X-Custom-Header) |
| Access-Control-Allow-Credentials | 是否允许携带凭据(Cookie、HTTP 认证信息),值为 true/false |
| Access-Control-Max-Age | 预检请求的缓存时间(秒),避免重复发送预检请求 |
4. CORS 实战实现
后端代码(处理预检请求 + 真实请求)
const http = require('http');
// 创建服务器
const server = http.createServer((req, res) => {
const headers = {
'Access-Control-Allow-Origin': 'http://localhost:5500', // 允许的源
'Access-Control-Allow-Methods': 'GET, PUT, OPTIONS', // 允许的方法
'Access-Control-Allow-Headers': 'Content-Type, X-Custom-Header', // 允许的头部
'Access-Control-Max-Age': '86400' // 预检请求缓存1天
};
// 处理OPTIONS预检请求
if (req.method === 'OPTIONS') {
res.writeHead(204, headers); // 204表示成功但无响应体
res.end();
return;
}
// 处理PUT真实请求
if (req.method === 'PUT' && req.url === '/data') {
let body = '';
// 接收请求体数据
req.on('data', chunk => {
body += chunk.toString();
});
// 数据接收完成
req.on('end', () => {
console.log('收到PUT数据:', body);
res.writeHead(200, headers);
res.end(JSON.stringify({
status: 'success',
message: 'CORS跨域请求成功',
data: body
}));
});
} else {
res.writeHead(404, headers);
res.end('Not Found');
}
});
// 启动服务器
const PORT = 3000;
server.listen(PORT, () => {
console.log(`CORS服务器运行在端口 ${PORT}`);
});
前端代码(发起 PUT 跨域请求)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CORS Preflight Example</title>
</head>
<body>
<script>
var xhr = new XMLHttpRequest();
// 配置PUT请求(复杂请求)
xhr.open('PUT', 'http://localhost:3000/data', true);
// 设置自定义Content-Type(触发预检请求)
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
// 设置自定义请求头
xhr.setRequestHeader('X-Custom-Header', 'custom-value');
// 发送JSON数据
xhr.send(JSON.stringify({key: "value"}));
// 监听请求完成
xhr.onload = function() {
if (xhr.status === 200) {
console.log('CORS请求成功:', xhr.response);
} else {
console.error('请求出错:', xhr.status, xhr.statusText);
}
};
// 监听错误
xhr.onerror = function() {
console.error('请求发生错误');
};
</script>
</body>
</html>
5. CORS 的优缺点
优点:
- 支持所有 HTTP 方法(GET/POST/PUT/DELETE 等)
- 安全可控:服务器可以精确控制允许的源、方法、头部,避免 JSONP 的 XSS 风险
- 符合标准:W3C 规范,所有现代浏览器都支持
- 无需前端额外处理:浏览器自动完成预检、请求发送等逻辑
缺点:
- 兼容性:不支持 IE10 以下的老旧浏览器
- 配置稍复杂:需要后端配合设置响应头,复杂请求需处理预检逻辑
6. 适用场景
现代前后端分离项目的首选方案,尤其是需要支持多种请求方法、自定义请求头的场景。
四、方案 3:WebSocket—— 实时通信的跨域神器
WebSocket 是一种全双工通信协议,它不属于 HTTP 协议,因此不受同源策略的限制,天然支持跨域,常用于实时通信场景(如聊天、弹幕、实时数据展示)。
1. WebSocket 的核心原理
WebSocket 的连接过程分为两步:
- 握手阶段:客户端先通过 HTTP 协议发送请求,请求头中包含
Upgrade: websocket,表示 “想要切换协议” - 协议切换:服务器同意后,返回 101 状态码(Switching Protocols),连接升级为 WebSocket 协议
- 通信阶段:建立双工通信,客户端和服务器可以双向实时发送消息,无需重复请求
2. WebSocket 实战实现
后端代码(Node.js + ws 库)
const http = require('http');
const fs = require('fs');
const path = require('path');
const WebSocket = require('ws');
// 创建HTTP服务器
const server = http.createServer((req, res) => {
// 提供前端页面
if (req.url === '/' || req.url === '/index.html') {
fs.readFile(path.join(__dirname, 'index.html'), (err, data) => {
if (err) {
res.writeHead(500);
res.end('Error loading index.html');
return;
}
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(data);
});
}
});
// 创建WebSocket服务器
const wss = new WebSocket.Server({ server, path: '/ws' });
// 监听连接
wss.on('connection', (ws) => {
console.log('客户端已连接');
// 监听客户端消息
ws.on('message', (msg) => {
console.log(`收到客户端消息:${msg}`);
// 回复客户端
ws.send(`Echo: ${msg}(服务器已收到)`);
});
// 监听连接关闭
ws.on('close', () => {
console.log('客户端已断开连接');
});
});
// 启动服务器
server.listen(8080, () => {
console.log('WebSocket服务器运行在 http://localhost:8080');
});
前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket Cross-Origin Demo</title>
</head>
<body>
<h1>WebSocket 跨域通信</h1>
<script>
// 连接WebSocket服务器(跨域)
const ws = new WebSocket('ws://localhost:8080/ws');
// 连接成功回调
ws.onopen = () => {
console.log('Connected to server');
ws.send('Hello from client!(跨域消息)');
};
// 接收服务器消息
ws.onmessage = (event) => {
console.log(`服务器回复:${event.data}`);
};
// 错误处理
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
// 连接关闭
ws.onclose = () => {
console.log('Disconnected from server');
};
</script>
</body>
</html>
3. WebSocket 的优缺点
优点:
- 天然跨域:不受同源策略限制
- 实时性高:全双工通信,服务器和客户端可主动发消息
- 性能好:一次连接,多次通信,无需重复建立连接
缺点:
- 适用场景有限:主要用于实时通信,不适合普通的接口请求
- 开发成本稍高:需要处理连接状态、重连、心跳等逻辑
4. 适用场景
实时聊天、弹幕、股票行情、物联网数据推送等需要双向实时通信的场景。
五、方案 4:postMessage—— 跨窗口通信的 “专属方案”
postMessage 是 HTML5 新增的 API,专门用于解决不同源窗口 /iframe 之间的通信问题,比如主页面和嵌入的 iframe 之间、多窗口之间的跨域数据传递。
1. postMessage 的核心原理
postMessage 允许不同源的窗口之间通过 “消息” 机制通信,核心逻辑:
- 发送方调用
window.postMessage(消息, 目标源)发送数据 - 接收方监听
message事件,获取发送的消息和来源 - 可通过
event.origin验证消息来源,确保安全
2. postMessage 实战实现
父页面(index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>父窗口</title>
</head>
<body>
<h1>This is parent window</h1>
<input type="text" class="inp" placeholder="输入要发送的消息">
<button class="send">发送信息到iframe</button>
<div class="contents">
<p>接收到的信息</p>
<ul class="messages"></ul>
</div>
<!-- 嵌入跨域的iframe -->
<iframe src="child.html" frameborder="3" class="child-iframe" height="600" width="800"></iframe>
<script>
// 监听message事件,接收iframe的消息
window.addEventListener('message', e => {
// 安全验证:只接收指定源的消息
// if (e.origin !== 'http://127.0.0.1:5501') return;
const box = document.querySelector('.messages');
box.innerHTML += `<li>收到:${e.data}, 来自${e.origin}</li>`;
});
// 获取iframe的window对象,发送消息
const win = document.querySelector('.child-iframe').contentWindow;
document.querySelector('.send').addEventListener('click', () => {
const msg = document.querySelector('.inp').value;
// 发送消息:第一个参数是消息,第二个参数是目标源(*表示所有源,建议指定具体域名)
win.postMessage(msg, '*');
document.querySelector('.inp').value = '';
});
</script>
</body>
</html>
子页面(child.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iframe子窗口</title>
</head>
<body>
<h1>This is iframe child page</h1>
<input type="text" class="inp" placeholder="输入要发送的消息">
<button class="send">发送信息到父窗口</button>
<div class="contents">
<p>接收到的信息</p>
<ul class="messages"></ul>
</div>
<script>
// 监听父窗口的消息
window.addEventListener('message', e => {
// 安全验证
// if (e.origin !== 'http://127.0.0.1:5501') return;
const box = document.querySelector('.messages');
box.innerHTML += `<li>收到:${e.data}, 来自${e.origin}</li>`;
});
// 发送消息到父窗口
document.querySelector('.send').addEventListener('click', () => {
const msg = document.querySelector('.inp').value;
// window.parent 指向父窗口
window.parent.postMessage(msg, '*');
document.querySelector('.inp').value = '';
});
</script>
</body>
</html>
3. postMessage 的优缺点
优点:
- 专门解决跨窗口 /iframe 通信问题
- 灵活可控:可验证消息来源,避免安全风险
- 支持任意类型数据:字符串、JSON、二进制数据等
缺点:
- 适用场景有限:仅用于窗口间通信,不适合普通接口请求
- iframe 性能差:嵌入 iframe 会增加页面性能开销,非必要不建议使用
4. 适用场景
- 主页面嵌入第三方 iframe(如支付窗口、广告窗口)
- 多窗口之间的跨域数据传递(如弹窗和主窗口)
- 第三方登录、支付回调等场景
六、方案 5:Vite 反向代理 —— 本地开发的 “最优解”
在前端本地开发阶段,跨域问题可以通过反向代理解决。Vite 内置了代理功能,无需后端配合,前端即可快速解决跨域。
1. 反向代理的核心原理
反向代理的本质是:
- 前端发起的请求先发送到 Vite 本地服务器(同源,无跨域)
- Vite 服务器将请求转发到后端接口服务器(服务器之间的请求不受同源策略限制)
- Vite 服务器将后端的响应返回给前端,从而绕过浏览器的跨域限制
2. Vite 代理实战配置
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
// 匹配所有/api开头的请求
'/api': {
target: 'http://localhost:3000', // 后端接口地址
changeOrigin: true, // 开启跨域模拟(修改请求头的Origin)
rewrite: (path) => path.replace(/^/api/, '') // 路径重写(可选)
}
}
}
})
3. 配置说明
target:后端接口的真实地址changeOrigin:设置为true时,Vite 会将请求头的Origin改为target的域名,避免后端识别跨域rewrite:路径重写,比如前端请求/api/user,会被转发为http://localhost:3000/user
4. 优缺点
优点:
- 本地开发专用:无需后端配合,前端一键配置
- 无跨域风险:完全绕过浏览器的同源策略
- 配置简单:Vite 内置功能,几行代码即可完成
缺点:
- 仅适用于本地开发:项目打包上线后,Vite 服务器不再运行,代理失效
- 仅解决前端开发阶段的跨域,无法解决生产环境问题
5. 适用场景
前端本地开发阶段,对接后端接口的跨域问题。
七、方案 6:Nginx 反向代理 —— 生产环境的 “终极方案”
如果说 Vite 代理是本地开发的专属方案,那么 Nginx 反向代理就是生产环境解决跨域的 “标配”。
1. Nginx 代理的核心原理
Nginx 作为前端静态资源服务器和反向代理服务器:
- 前端页面和 Nginx 运行在同一域名(同源),前端请求发送到 Nginx
- Nginx 将接口请求转发到后端服务器(服务器之间无跨域限制)
- Nginx 将后端响应返回给前端,实现跨域请求的 “伪装”
2. Nginx 配置实战
server {
listen 80; # 监听80端口
server_name localhost; # 服务器域名
# 前端静态资源(打包后的dist文件)
location / {
root /usr/share/nginx/html; # 前端静态资源路径
index index.html; # 默认首页
try_files $uri $uri/ /index.html; # 解决前端路由刷新404
}
# 核心:代理/api请求到后端接口
location /api/ {
# 后端接口地址
proxy_pass https://api.example.com/;
# 修改请求头的Host,避免后端识别跨域
proxy_set_header Host $host;
# 传递真实客户端IP
proxy_set_header X-Real-IP $remote_addr;
# 传递IP链(多层代理时用)
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 支持HTTPS
proxy_set_header X-Forwarded-Proto $scheme;
# 关闭缓存(调试时用)
proxy_cache_bypass $http_upgrade;
}
}
3. 优缺点
优点:
- 生产环境专用:稳定、高性能,支持高并发
- 无前端侵入:前端代码无需修改,仅需配置 Nginx
- 功能强大:可同时处理静态资源和接口代理,还能配置缓存、限流等
缺点:
- 需要服务器权限:需配置 Nginx 服务器,前端开发者可能无权限操作
- 配置稍复杂:需了解 Nginx 基本语法
4. 适用场景
- 生产环境前端项目的跨域问题
- 公司内部系统对接,不适合配置 CORS 白名单的场景
八、7 种跨域方案对比与选型建议
为了方便大家快速选择合适的方案,整理了以下对比表:
表格
| 方案 | 支持方法 | 兼容性 | 安全度 | 适用场景 |
|---|---|---|---|---|
| JSONP | 仅 GET | 极好(IE6+) | 低(XSS 风险) | 兼容老旧浏览器 |
| CORS | 所有方法 | 良好(IE10+) | 高(精准控制) | 现代前后端分离项目(首选) |
| WebSocket | 双向通信 | 良好(IE10+) | 中 | 实时通信(聊天、弹幕) |
| postMessage | 无(消息通信) | 良好(IE8+) | 中(需验证 origin) | 跨窗口 /iframe 通信 |
| Vite 代理 | 所有方法 | 仅本地开发 | 高 | 前端本地开发 |
| Nginx 代理 | 所有方法 | 生产环境 | 高 | 生产环境跨域 |
选型建议:
- 本地开发:优先用 Vite/ Webpack 反向代理
- 现代项目生产环境:优先用 CORS(后端配置)
- 生产环境无法修改后端:用 Nginx 反向代理
- 实时通信场景:用 WebSocket
- 跨窗口 /iframe 通信:用 postMessage
- 兼容老旧浏览器:用 JSONP(迫不得已时)
九、总结
跨域问题的本质是浏览器的同源策略,而解决跨域的核心思路无非两种:
- 绕开同源策略(JSONP、WebSocket、代理)
- 让服务器明确允许跨域(CORS)
在实际开发中,无需掌握所有方案,只需根据场景选择最合适的:现代项目优先用 CORS + 本地代理,特殊场景用 WebSocket/postMessage,生产环境用 Nginx 兜底。
希望本文能帮助你彻底搞懂跨域,下次遇到跨域问题不再迷茫!