跨域实战指南:从踩坑到优雅解决(前端必看)

先搞懂跨域到底是个啥

跨域不是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 });
});
`

踩坑记录

  • 如果前端用了withCredentials: true(携带cookie),后端Access-Control-Allow-Origin不能是*,必须明确指定域名。
  • 预检请求(OPTIONS)如果没正确处理,浏览器会报“CORS preflight did not succeed”。我第一次遇到时,后端日志里根本没有OPTIONS请求,以为是前端问题。
  • 生产环境一定要限制允许的域名,否则会被别人随便调用接口。
  • 另一个坑: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 是关键,它会伪造请求头,让后端以为请求来自同源。如果不加,有些后端会校验RefererOrigin头。踩坑:我遇到过changeOrigin没加,后端返回的数据正常,但浏览器报跨域,查了半小时才发现。

    开发环境代理的坑

  • 只解决开发环境问题,生产环境还是要让后端配CORS或Nginx
  • 如果代理路径写错,比如/api/api/有区别,会导致404
  • 有些代理配置不支持WebSocket,需要显式声明ws: true
  • 方案四: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;
    }
    }
    `

    踩坑记录

  • proxy_pass后面有无/很关键:有/会去掉匹配前缀,没有则保留。比如/api/user → 有/会变成/user,没有则变成/api/user
  • 如果后端有WebSocket,需要额外配置proxy_http_version 1.1Upgrade头。
  • 我遇到过线上跨域报错,结果是Nginx配置里少了proxy_set_header,后端拿着错误的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);
    });
    `

    踩坑记录

  • 一定要校验event.origin,否则会被恶意网站利用
  • 如果iframe加载慢,消息可能丢失,需要等onload事件
  • 不要用event.origin直接做字符串拼接,有安全风险
  • (总结前配图:一张决策流程图,从“是否需要兼容老浏览器”开始,分支到JSONP、CORS、Nginx、WebSocket等方案,标注“跨域方案选择指南”)

    总结一下,你可以立刻用的三个点

  • 开发用代理,生产用Nginx:Vite/Webpack代理解决本地调试,Nginx反向代理统一域名,省去CORS配置的麻烦。
  • CORS一定要配预检+域名白名单:别偷懒用*,生产环境明确指定允许的域名,否则安全审计过不了。
  • 遇到跨域先看浏览器控制台:错误信息会告诉你缺哪个头,比如“Missing Allow Origin”就是少了Access-Control-Allow-Origin`。别像我一样瞎猜半小时。
  • 如果你还在为跨域抓狂,试试按这个顺序排查:先看开发环境代理是否生效 → 再看后端CORS是否配置 → 最后检查Nginx。90%的问题出在前两步。

    滚动至顶部