先搞懂跨域到底是个啥
跨域不是bug,是浏览器的安全机制——同源策略。它规定:协议、域名、端口,三者必须一致
<
p>,否则浏览器会拒绝响应。比如:
http://a.com请求http://b.com→ 跨域http://a.com:8080请求http://a.com:3000→ 跨域(端口不同)https://a.com请求http://a.com→ 跨域(协议不同)
这个设计真的反人类:明明后端接口能正常返回数据,浏览器就是硬生生拦截,还报个“CORS Missing Allow Origin”。我第一次遇到时,查了半小时以为是后端写错了。
方案一:JSONP(老古董但还能用)
JSONP的原理是利用<script>标签不受同源策略限制,把数据塞进回调函数里。当年没有CORS的时候,这就是唯一方案。
“javascript
// 前端:动态创建script标签
function jsonpRequest(url, callbackName) {
return new Promise((resolve, reject) => {
// 定义全局回调函数
window[callbackName] = function(data) {
resolve(data);
// 清理:删除script标签和全局函数
document.body.removeChild(script);
delete window[callbackName];
};
const script = document.createElement('script');
script.src = ${url}
?callback=${callbackName};
script.onerror = () => reject(new Error('JSONP请求失败'));
document.body.appendChild(script);
});
}
// 使用
jsonpRequest('http://api.example.com/user', 'handleUser')
.then(data => console.log(data))
.catch(err => console.error(err));
`
为什么这么写? 因为JSONP只支持GET请求,而且需要后端配合返回callback({ data })格式。踩坑点:回调函数名不能重复,否则会覆盖;如果后端返回慢,页面可能已经卸载了回调函数。
优点:兼容性好,IE6都能用
缺点:只能GET、有安全风险(容易XSS)、不方便调试
个人吐槽:现在2024年了,除非你要兼容老掉牙的IE或者对接某些政府项目,否则别用JSONP。我上次在一个新项目里看到同事用JSONP,差点血压飙升。
(核心配图:一张对比图,左边是JSONP的script标签原理,右边是CORS的请求头交互,标注“JSONP vs CORS:谁更现代?”)
方案二:CORS(最正统的解决方案)
CORS是现代跨域的标配。前端什么都不用改,关键在后端配置响应头。但很多前端不知道后端怎么配,导致沟通成本极高。
`javascript
// Node.js (Express) 后端配置示例
const express = require('express');
const app = express();
// 简单请求:直接加Access-Control-Allow-Origin
app.use((req, res, next) => {
// 生产环境不要用*,要指定域名
res.setHeader('Access-Control-Allow-Origin', 'https://your-frontend.com');
// 允许携带cookie
res.setHeader('Access-Control-Allow-Credentials', 'true');
// 复杂请求:预检请求(OPTIONS)处理
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// 预检缓存时间(秒),减少重复预检
res.setHeader('Access-Control-Max-Age', '86400');
return res.status(204).end();
}
next();
});
app.get('/api/user', (req, res) => {
res.json({ name: '张三', age: 30 });
});
`
踩坑记录:
(携带cookie),后端Access-Control-Allow-Origin不能是*,必须明确指定域名。 另一个坑:Nginx代理也可能干扰CORS。如果你前端请求经过Nginx,要在Nginx层也配置这些头。
`nginx
Nginx配置示例
location /api/ {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
add_header Access-Control-Max-Age 86400;
if ($request_method = 'OPTIONS') {
return 204;
}
proxy_pass http://backend-server;
}
`
方案三:开发环境代理(Webpack/Vite)
开发时最常用的是代理,不需要后端改任何东西。Webpack的devServer.proxy和Vite的server.proxy原理一样:启动一个本地服务器,把请求转发到目标地址。
`javascript`
// Vite配置(vite.config.js)
export default {
server: {
proxy: {
'/api': {
target: 'http://api.example.com',
changeOrigin: true, // 改变请求头中的Host
rewrite: (path) => path.replace(/^\/api/, ''),
// 踩坑:如果后端有路径重写,需要调整rewrite
},
'/socket.io': {
target: 'ws://api.example.com',
ws: true, // 支持WebSocket代理
}
}
}
};
为什么这么写? changeOrigin: true 是关键,它会伪造请求头,让后端以为请求来自同源。如果不加,有些后端会校验Referer或Origin头。踩坑:我遇到过changeOrigin没加,后端返回的数据正常,但浏览器报跨域,查了半小时才发现。
开发环境代理的坑:
和/api/有区别,会导致404 方案四:Nginx反向代理(生产环境常用)
生产环境不用CORS,直接用Nginx做反向代理,把前端和后端统一到一个域名下。
`nginx
将/api请求转发到后端,同时前端静态文件也由Nginx托管
server {
listen 80;
server_name your-app.com;
# 前端静态文件
root /var/www/frontend;
index index.html;
# API反向代理
location /api/ {
proxy_pass http://backend-server:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 超时设置(踩坑:默认60秒不够)
proxy_connect_timeout 10s;
proxy_read_timeout 120s;
}
# SPA路由处理:所有非文件请求都返回index.html
location / {
try_files $uri $uri/ /index.html;
}
}
`
踩坑记录:
后面有无/很关键:有/会去掉匹配前缀,没有则保留。比如/api/user → 有/会变成/user,没有则变成/api/user。 和Upgrade头。 ,后端拿着错误的Host做校验。方案五:WebSocket(特殊场景)
WebSocket本身不受同源策略限制,但如果前端通过HTTP请求升级到WebSocket,浏览器还是会检查。解决方案是在后端明确允许所有源。
`javascript
// 前端WebSocket
const socket = new WebSocket('ws://api.example.com/chat');
socket.onopen = () => {
console.log('连接成功');
socket.send(JSON.stringify({ type: 'join', room: 'general' }));
};
socket.onmessage = (event) => {
console.log('收到消息:', event.data);
};
// 后端(Node.js + ws库)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws, req) => {
// 设置允许所有源(生产环境要限制)
ws.on('headers', (headers) => {
headers.push('Access-Control-Allow-Origin: *');
});
ws.on('message', (message) => {
console.log('收到:', message.toString());
});
});
`
为什么这么写? 因为WebSocket握手阶段还是HTTP请求,如果跨域,浏览器会检查响应头。如果不加Access-Control-Allow-Origin,握手会失败。踩坑:我用Socket.io时,客户端一直重连,以为是网络问题,结果是跨域。
方案六:PostMessage(iframe跨域)
如果你要在iframe里跨域通信,比如嵌入第三方页面,可以用window.postMessage。
`javascript
// 父页面(a.com)
const iframe = document.getElementById('myIframe');
iframe.contentWindow.postMessage('你好,我是父页面', 'https://b.com');
// 监听回复
window.addEventListener('message', (event) => {
if (event.origin !== 'https://b.com') return; // 安全校验
console.log('收到子页面回复:', event.data);
});
// 子页面(b.com)
window.addEventListener('message', (event) => {
if (event.origin !== 'https://a.com') return;
console.log('收到父页面消息:', event.data);
event.source.postMessage('收到,我是子页面', event.origin);
});
`
踩坑记录:
,否则会被恶意网站利用 事件 直接做字符串拼接,有安全风险(总结前配图:一张决策流程图,从“是否需要兼容老浏览器”开始,分支到JSONP、CORS、Nginx、WebSocket等方案,标注“跨域方案选择指南”)
总结一下,你可以立刻用的三个点
,生产环境明确指定允许的域名,否则安全审计过不了。 如果你还在为跨域抓狂,试试按这个顺序排查:先看开发环境代理是否生效 → 再看后端CORS是否配置 → 最后检查Nginx。90%的问题出在前两步。