跨域问题的根源是浏览器的同源策略(Same-Origin Policy) 。它是浏览器最核心的安全机制,限制了一个源下的文档或脚本与另一个源的资源交互。
-
源 的定义:协议 + 域名 + 端口 三者完全一致。
-
同源策略主要限制:
- 不同源的 Cookie、LocalStorage、IndexedDB 无法互相访问。
- 不同源的 DOM 无法通过 JS 直接获取(iframe 跨域隔离)。
- 不同源的 AJAX 请求(
XMLHttpRequest、fetch)默认被拦截,无法读取响应内容。
有些标签可以跨域加载资源,但无法读取内容:
<script>、<img>、<link>、<video>、<audio>、<iframe> 等。
所有跨域方案都是在安全的前提下绕过同源策略限制。
一、JSONP(JSON with Padding)
原理
利用 <script> 标签不受同源策略限制的特性,动态创建一个 script 标签,向服务端请求一段可执行的 JS 代码,代码中调用本地预先定义好的回调函数,并把数据作为参数传入。
前端实现
js
function jsonp(url, callbackName) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
// 定义全局回调函数
window[callbackName] = function(data) {
resolve(data);
document.body.removeChild(script);
delete window[callbackName];
};
// 拼接 URL(服务端需返回类似 `callbackName({...})`)
script.src = `${url}?callback=${callbackName}`;
document.body.appendChild(script);
});
}
// 调用
jsonp('https://api.example.com/data', 'handleData').then(data => console.log(data));
服务端处理(Node.js 示例)
js
app.get('/data', (req, res) => {
const callback = req.query.callback;
const data = { user: 'Alice' };
res.end(`${callback}(${JSON.stringify(data)})`);
});
优缺点
-
优点:兼容极老的浏览器,不依赖现代浏览器特性。
-
缺点:
- 只支持 GET 请求。
- 错误处理困难(script 加载失败没有 HTTP 状态码,需通过
onerror和定时器兜底)。 - 安全风险:被调用方必须完全信任,可能执行恶意脚本。
- 无法设置自定义请求头。
如今几乎已被 CORS 取代,但在某些遗留系统或第三方 JS SDK 中可能还有应用。
二、CORS(Cross-Origin Resource Sharing)
这是现代解决跨域的标准方案,由浏览器和服务器协作完成。核心是服务器通过 HTTP 响应头告知浏览器:"允许某个源访问我"。
两类请求
浏览器将跨域请求分为两类,行为不同。
1. 简单请求
同时满足以下条件:
- 方法:
GET、HEAD、POST之一。 - 头部只包含安全的:
Accept、Accept-Language、Content-Language、Content-Type(且值必须是text/plain、multipart/form-data、application/x-www-form-urlencoded之一)。 - 没有
ReadableStream对象。
流程 :浏览器直接发请求,自动在请求头加上 Origin(当前网页的来源),服务器若允许则返回 Access-Control-Allow-Origin 头部,浏览器检查该头决定是否把响应交给前端 JS。
2. 预检请求(Preflight)
非简单请求(如 PUT、DELETE、带自定义头 Authorization、Content-Type: application/json)会先发一个 OPTIONS 方法探测,确认服务器允许后才发正式请求。
预检阶段头部:
-
请求头:
Origin、Access-Control-Request-Method(正式请求的方法)、Access-Control-Request-Headers(正式请求的自定义头)。 -
响应头:
Access-Control-Allow-Origin:允许的源,*代表所有(但不能与credentials共存)。Access-Control-Allow-Methods:允许的方法,如GET, POST, PUT。Access-Control-Allow-Headers:允许的头部。Access-Control-Max-Age:预检结果缓存时间(秒),期间不再发预检。
正式请求 与简单请求类似,也会带 Origin,服务器需同样返回 Access-Control-Allow-Origin。
与 Cookie 相关(Credentials)
默认跨域请求不携带 Cookie,需前端设置 withCredentials(XHR)或 credentials: 'include'(fetch),同时服务器返回:
js
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: 具体的源(不能为 *)
常见 CORS 配置示例(Express)
js
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://example.com');
res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type,Authorization');
res.header('Access-Control-Allow-Credentials', 'true');
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
优缺点
- 优点:标准、安全、支持所有 HTTP 方法、精细控制。
- 缺点:需要服务端配合;预检请求增加一次往返(但可缓存)。
三、代理转发(Proxy)
核心思想:同源策略只限制浏览器端,服务端没有此限制。通过配置代理服务器,把前端的请求转发到目标服务器,浏览器只与同源的代理通信。
1. 开发环境(Webpack DevServer / Vite)
js
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true, // 修改请求头的 Origin 为目标地址
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
};
前端请求 /api/user,DevServer 将其转发到 https://api.example.com/user,绕过浏览器跨域限制。
2. 生产环境(Nginx 反向代理)
js
location /api/ {
proxy_pass https://api.example.com/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
所有指向当前域 /api/ 的请求被转发,对浏览器来说仍是同源。
优缺点
- 优点:前端无侵入,可隐藏真实后端地址,支持复杂场景。
- 缺点:增加了服务器维护成本;对 WebSocket 等也需额外配置。
四、WebSocket
WebSocket 协议不实行同源策略,但握手阶段会发送 Origin 头。服务器可以通过检查 Origin 来决定是否允许连接。
js
// 前端
const ws = new WebSocket('wss://api.example.com/socket');
ws.onmessage = (e) => console.log(e.data);
// 服务端(Node.js ws 库)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws, req) => {
const origin = req.headers.origin;
// 可校验 origin 拒绝未知来源
ws.send('connected');
});
适用场景
- 实时通信,如聊天、协作编辑、行情推送。跨域通信只是其附带特性。
五、postMessage(跨文档消息传递)
用于不同窗口/iframe 之间的安全通信,即使不同源也可以传输数据。
发送方
js
// 父页面
const iframe = document.querySelector('iframe');
iframe.contentWindow.postMessage('Hello from parent', 'https://child.example.com');
接收方
js
window.addEventListener('message', (event) => {
// 始终验证来源!
if (event.origin !== 'https://parent.example.com') return;
console.log('收到:', event.data);
// 可以回复
event.source.postMessage('Hi back', event.origin);
});
关键安全措施
- 发送时必须指定精确的
targetOrigin,不要用*。 - 接收时务必检查
event.origin,防止恶意页面伪造消息。
应用
- 页面与跨域 iframe 通信(如微前端、第三方支付窗口)。
- 与 Web Worker 通信(专用 Worker 默认同源,Shared Worker 可能跨域)。
六、其他辅助方案(简要)
1. document.domain(已逐步废弃)
将两个不同子域的页面的 document.domain 设置为相同主域,可实现跨子域通信。但现代浏览器严格限制,许多 API 不再支持。
2. window.name
在一个窗口的生命周期里,window.name 属性保持不变,即使跳转跨域页面。结合 iframe 可实现跨域传递数据,但属于 hack 手法,已不推荐。
3. location.hash
通过修改 url 的 hash 和 onhashchange 事件在不同域之间传递数据,同样属于 hack。
七、各方案对比总结
| 方案 | 适用场景 | 安全性 | 是否需服务端 | 现代推荐度 |
|---|---|---|---|---|
| CORS | 同源 AJAX 调用 | 高(精细控制) | 是(响应头) | ⭐⭐⭐⭐⭐ |
| 代理 | 开发/生产环境通用 | 高(无浏览器限制) | 需代理服务器 | ⭐⭐⭐⭐⭐ |
| JSONP | 老旧浏览器兼容 | 低(易 XSS) | 是(返回 JS) | ⭐ |
| WebSocket | 实时双工通信 | 中(可校验 Origin) | 需 WS 服务 | ⭐⭐⭐⭐ |
| postMessage | 跨窗口/iframe 通信 | 高(需验证 origin) | 否 | ⭐⭐⭐⭐⭐ |
实际开发中, "CORS + 代理" 是解决 95% 跨域问题的黄金组合;剩下 5% 可能会用 postMessage 或 WebSocket。理解了每个方案的本质和限制,就能根据场景选择最合适的办法。