跨域解决方案:9 种方法及 CORS、代理实战

跨域解决方案的核心不是“关闭浏览器限制”,而是让服务器明确授权,或通过代理把请求转换为同源请求。 当前项目优先考虑 CORS、Nginx 反向代理和开发服务器代理;窗口或 iframe 通信使用 window.postMessage();JSONP、document.domain 等旧方案只适合特定历史项目。本文面向前端初学者和需要排查接口跨域的开发者,讲清原理、代码与选型方法。

核心要点

  • 同源要求协议、主机名、端口三项一致,路径不同不影响同源。
  • 跨域是浏览器执行的安全机制,不等于服务器没有收到请求。
  • 普通接口优先用 CORS;希望浏览器只看到同源地址时,使用反向代理。
  • 携带 Cookie 时,CORS 不能使用 Access-Control-Allow-Origin: *。
  • 不推荐在新项目中使用 document.domain、JSONP、window.name + iframe 或 location.hash + iframe。

什么是跨域?先理解同源策略

当一个页面的源(origin)与它要读取资源的源不一致时,就产生了跨域场景。源由协议、主机名和端口共同决定,而不是只看域名。两个不同域名即使解析到同一个 IP 地址,也仍然不同源。

假设当前页面是 https://www.example.com/app/index.html:

请求地址 是否同源 原因
https://www.example.com/api/user 是 协议、主机名、默认端口均相同
http://www.example.com/api/user 否 HTTPS 与 HTTP 协议不同
https://api.example.com/api/user 否 主机名不同
https://www.example.com:8443/api/user 否 端口不同

MDN 对同源策略的说明指出,同源策略会限制一个源如何与另一个源的资源交互。它是一道重要的浏览器安全边界,但不能替代完整的 XSS、CSRF 防护。

常见限制包括:脚本不能随意读取跨源页面的 DOM;不能任意读取跨源的 Cookie、localStorage 或 IndexedDB;跨源 fetch/XHR 的响应需要满足 CORS 等授权条件。需要注意,“允许嵌入资源”不等于“允许读取数据”:图片、脚本等资源可能被加载,不代表页面可以无限制读取其内容。

浏览器页面 → 发出跨源请求 → 服务器处理并响应
                         ↓
              浏览器检查 CORS 等授权信息
                         ↓
               允许 JS 读取,或拦截响应

图 1:跨域报错可能发生在响应返回之后,不能仅凭 Network 中的状态码判断。

9 种跨域解决方案对比

方案 典型场景 优点 主要限制 新项目建议
CORS 前后端分离、开放 API 标准化,前端改动小 需要服务端正确配置 优先
Nginx 反向代理 生产环境统一入口 对浏览器表现为同源,便于集中管理 需要部署和运维权限 优先
Node.js 开发代理 本地开发、Vite/Webpack 项目 配置简单,不改动后端 通常只解决开发环境 推荐
window.postMessage() 新窗口、父页面与 iframe 通信 原生支持双向消息 必须校验来源和消息格式 推荐
JSONP 只需 GET 的旧系统 实现简单,兼容旧浏览器 仅限 GET,安全和维护成本高 谨慎
WebSocket 实时消息、长连接 支持双向实时通信 不是普通 HTTP API 的通用替代品 按业务使用
document.domain 同主域的历史子域页面 旧项目可能已有实现 已弃用,削弱源隔离 不推荐
window.name + iframe 历史页面数据传递 可绕过部分旧限制 流程复杂,安全和维护困难 不推荐
location.hash + iframe 历史 iframe 通信 不依赖现代消息 API 能力有限,容易产生安全问题 不推荐

方案一:CORS 跨域资源共享

CORS(Cross-Origin Resource Sharing,跨源资源共享)通过 HTTP 响应头,让服务器声明哪些源可以读取响应。它是跨源接口请求的标准方案,关键配置在服务端,不是在前端请求里随意加一个同名请求头。MDN 的 CORS 指南还区分了简单请求、预检请求和携带凭据的请求。

1. 普通请求的服务端配置

下面以 Express 为例,只允许一个明确的前端源:

const express = require('express');
const app = express();

const allowedOrigin = 'https://www.example.com';

app.use((req, res, next) => {
  res.set({
    'Access-Control-Allow-Origin': allowedOrigin,
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    'Vary': 'Origin'
  });

  if (req.method === 'OPTIONS') {
    return res.sendStatus(204);
  }

  next();
});

Access-Control-Allow-Origin 是最关键的响应头。只开放公共数据时可以使用 *,但涉及用户身份或敏感数据时,应采用明确来源白名单,不要直接反射任意 Origin。

2. 为什么浏览器先发送 OPTIONS?

当请求不满足“简单请求”条件时,浏览器通常会先发送 OPTIONS 预检请求,确认服务器允许相应方法和请求头,再决定是否发送实际请求。例如,使用 Authorization,或以 application/json 发送某些 POST 请求时,经常会看到预检。

因此,后端路由和网关必须正确处理 OPTIONS。只看到一次 OPTIONS 并不是接口被重复调用,而是浏览器在执行预检流程。

3. 携带 Cookie 的配置

前端需要主动开启凭据:

// fetch
fetch('https://api.example.com/user', {
  credentials: 'include'
});

// axios
axios.get('https://api.example.com/user', {
  withCredentials: true
});

服务端还需要返回:

Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Credentials: true

这里有两个高频错误:

  1. 携带凭据时不能使用 Access-Control-Allow-Origin: *,必须返回明确来源。
  2. CORS 放行不代表 Cookie 一定能发送,Cookie 仍受 SameSite、Secure 和浏览器第三方 Cookie 策略约束。

方案二:Nginx 反向代理

如果页面和接口都通过 https://www.example.com 对外提供,浏览器看到的请求就是同源请求。Nginx 接收 /api/ 请求后,再将其转发到后端服务:

server {
    listen 443 ssl;
    server_name www.example.com;

    location /api/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

前端只请求同源路径:

fetch('/api/user').then(response => response.json());

代理不是“让浏览器忽略同源策略”,而是改变了浏览器实际请求的入口。生产环境使用代理时,还要检查 HTTPS、路径映射、超时、请求体大小和真实客户端 IP 等配置。

方案三:Node.js 开发服务器代理

本地页面运行在 http://localhost:5173,接口运行在 http://localhost:3000 时,端口不同会导致跨域。以 Vite 为例,可在 vite.config.js 中配置:

export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true
      }
    }
  }
};

开发服务器会代理 /api 请求,因此无需为了本地开发给测试接口开放任意来源。这份配置通常不会自动作用于生产构建,上线后仍应选择 CORS 或生产反向代理。

如果你还没有可调试的后端接口,可以先阅读站内的 Node.js 和 MySQL 增删查改 API 教程,完成基础接口搭建后再接入上述代理。

方案四:window.postMessage 跨窗口通信

postMessage 适用于页面与新窗口、父页面与 iframe 之间的数据传递,不是普通 AJAX 接口跨域的替代品。

发送方应指定精确的目标源:

const childWindow = window.open('https://child.example.com', 'child');

childWindow.postMessage(
  { type: 'USER_READY', payload: { id: 42 } },
  'https://child.example.com'
);

接收方必须校验 origin,必要时同时校验 source 和数据结构:

window.addEventListener('message', (event) => {
  if (event.origin !== 'https://parent.example.com') return;
  if (event.source !== window.parent) return;
  if (event.data?.type !== 'USER_READY') return;

  console.log('收到可信消息:', event.data.payload);
});

MDN 对 postMessage 的安全说明强调:发送敏感消息时不应随意把 targetOrigin 设为 *,接收方也应验证发送者身份和消息语法,避免数据泄露或注入风险。

其他五种方案:JSONP、WebSocket 与旧式 iframe 方案

方案五:JSONP

JSONP 利用 <script> 标签加载跨源资源的能力,由服务器返回一段调用指定回调函数的 JavaScript:

<script>
  function handleUser(data) {
    console.log(data);
  }
</script>
<script src="https://api.example.com/user?callback=handleUser"></script>

服务器返回的内容类似:

handleUser({ "id": 42, "name": "Alice" });

JSONP 通常只支持 GET,而且会把第三方响应作为脚本执行。只应连接可信端点,不要用它传输敏感数据,也不要把未经严格校验的回调名直接拼接进响应。

方案六:WebSocket

WebSocket 适用于聊天、实时行情和协同编辑等长连接场景。它不受 CORS 的同一套读取规则约束,但这不代表可以接受任意网站连接。服务端仍应校验 Origin,并完成身份认证和消息授权。不要用 WebSocket 重写普通 CRUD 接口,仅仅为了绕开 CORS。

方案七:document.domain

过去,同主域的不同子域页面会同时设置 document.domain = 'example.com',尝试放宽部分同源检查。但该方式会削弱浏览器提供的安全保护,MDN 已明确将 document.domain 标记为弃用,新项目不应采用。

方案八:window.name + iframe

该旧方案利用窗口名称在导航后仍可保留的特性传递数据,往往需要空白代理页和多次 iframe 导航。它实现复杂、调试困难,也容易引入不受信数据,建议迁移到 postMessage 或标准接口方案。

方案九:location.hash + iframe

该方案通过修改 iframe 的 URL 片段传递消息,曾用于旧浏览器中的跨窗口通信。它能传递的数据有限,还可能暴露于历史记录、日志或页面脚本。现代项目应优先使用带来源校验的 postMessage。

如何选择跨域解决方案

可以按下面的顺序判断:

  1. 前后端分离的正式接口:优先 CORS,服务端按环境配置来源白名单。
  2. 生产环境希望统一域名入口:使用 Nginx 或网关反向代理。
  3. 只有本地开发跨域:使用 Vite、Webpack 或 Node.js 开发服务器代理。
  4. iframe 或新窗口传消息:使用 postMessage,同时校验来源和数据。
  5. 实时双向业务:按需求使用 WebSocket,而不是为了规避 CORS。
  6. 维护历史系统:评估 JSONP 等旧方案的风险,并制定迁移计划。

简单说:API 选 CORS 或代理,窗口通信选 postMessage,实时通信选 WebSocket。

跨域报错排查清单

遇到跨域问题时,按以下顺序检查:

  • 在控制台确认是 CORS、混合内容、Cookie,还是普通网络错误。
  • 对比页面与接口的协议、主机名和端口,确认是否真正跨源。
  • 在 Network 中检查实际请求的 Origin,不要只看前端配置了哪些请求头。
  • 检查响应中的 Access-Control-Allow-Origin 是否精确匹配页面源。
  • 如果有预检,确认 OPTIONS 返回成功,并允许实际方法和请求头。
  • 携带 Cookie 时,确认前后端凭据配置正确,且未使用通配符 *。
  • 检查网关、CDN 或后端是否重复、覆盖了 CORS 响应头。
  • 使用代理时,确认生产环境也部署了对应规则,而非只有本地配置有效。

排查接口返回结构时,也可以使用站内的 JSON 格式化工具,先确认响应是否为合法 JSON,再继续检查 CORS 响应头。

常见问题 FAQ

跨域是前端问题还是后端问题?

跨域限制由浏览器执行,但解决方案通常需要前后端配合。CORS 主要由服务端响应头授权,代理则由开发服务器或部署网关实现。

接口返回 200,为什么前端仍提示跨域?

服务器可能已经处理并返回响应,但浏览器发现响应缺少有效 CORS 授权,于是阻止 JavaScript 读取。应同时检查控制台和响应头。

CORS 和反向代理有什么区别?

CORS 保留跨源请求,由服务器授权浏览器读取响应;反向代理让浏览器请求同源入口,再由服务器转发到后端。两者可按架构选用。

JSONP 可以解决 POST 跨域吗?

通常不能。JSONP 通过脚本标签发起 GET 请求,只适合特定旧式接口。需要 POST、PUT 或 DELETE 时,应使用 CORS 或服务器代理。

两个域名指向同一 IP,算同源吗?

不算。同源比较的是协议、主机名和端口,不比较服务器 IP。即使部署在同一台机器上,不同域名仍属于不同源。

结语

跨域并不可怕,关键是先识别业务场景,再选择正确的跨域解决方案:普通接口优先 CORS,统一入口使用反向代理,本地开发使用开发代理,iframe 通信使用安全的 postMessage。 不建议为了省事关闭浏览器安全策略,也不应在新项目中继续采用已经弃用或高风险的旧方案。

如果你正在整理自己的前端知识体系,可以继续访问 yfcov.com 的技术博客与实用工具,把本文的选型表和排查清单作为日常调试参考。

发布技术文章后,如果还想继续提升内容的关键词布局与自然搜索表现,可以参考站内的 Google SEO 完整指南:关键词、内容、外链与分析策略。

Picture of ByronMann

ByronMann

我是懒洋洋爱睡觉,爱写代码的独立站运营。5年独立站实战,8年码农底子,左手代码右手流量,用技术思维做运营。代码写得动,懒觉睡得香。

目录