万字长文吃透前端跨域:从原理到实战,7 种方案全解析

3 阅读7分钟

在前后端分离架构成为主流的今天,跨域几乎是每个前端开发者绕不开的问题。浏览器的同源策略像一道 “安全墙”,保护着用户数据,却也给我们的开发带来了不少挑战。本文将从跨域的根源讲起,结合实战代码,系统拆解 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 的连接过程分为两步:

  1. 握手阶段:客户端先通过 HTTP 协议发送请求,请求头中包含 Upgrade: websocket,表示 “想要切换协议”
  2. 协议切换:服务器同意后,返回 101 状态码(Switching Protocols),连接升级为 WebSocket 协议
  3. 通信阶段:建立双工通信,客户端和服务器可以双向实时发送消息,无需重复请求

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 代理所有方法生产环境生产环境跨域

选型建议:

  1. 本地开发:优先用 Vite/ Webpack 反向代理
  2. 现代项目生产环境:优先用 CORS(后端配置)
  3. 生产环境无法修改后端:用 Nginx 反向代理
  4. 实时通信场景:用 WebSocket
  5. 跨窗口 /iframe 通信:用 postMessage
  6. 兼容老旧浏览器:用 JSONP(迫不得已时)

九、总结

跨域问题的本质是浏览器的同源策略,而解决跨域的核心思路无非两种:

  • 绕开同源策略(JSONP、WebSocket、代理)
  • 让服务器明确允许跨域(CORS)

在实际开发中,无需掌握所有方案,只需根据场景选择最合适的:现代项目优先用 CORS + 本地代理,特殊场景用 WebSocket/postMessage,生产环境用 Nginx 兜底。

希望本文能帮助你彻底搞懂跨域,下次遇到跨域问题不再迷茫!