人人都会AI编程

16.4 跨域问题与解决方案

更新时间:2026-07-11

在前端开发中,几乎每个开发者都会遇到这样一个场景:本地启动的开发服务器运行在 http://localhost:3000,而后端 API 运行在 http://localhost:8080,浏览器控制台无情地抛出一行红字:

Access to fetch at 'http://localhost:8080/api' from origin 'http://localhost:3000' 
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header...

这就是跨域问题。理解它的本质和解决方案,是前后端协作开发的基础技能。

16.4.1 同源策略的本质与限制

同源策略(Same-Origin Policy) 是浏览器的核心安全机制。它规定:一个源的文档或脚本,只能与同源的资源进行交互。

什么是“同源”?必须同时满足三个条件:

  • 协议相同(如 http vs https
  • 域名相同(如 www.example.com 的主域名和子域名都算不同源)
  • 端口相同(如 3000 vs 8080

举例来说,http://www.example.com:80/page 与以下地址的同源性判断:

| 待比较地址 | 是否同源 | 原因 |
|---|---|---|
| http://www.example.com:80/other | ✅ 同源 | 协议、域名、端口完全相同 |
| https://www.example.com:80/page | ❌ 不同源 | 协议不同(https vs http) |
| http://api.example.com:80/page | ❌ 不同源 | 域名不同(子域名不同) |
| http://www.example.com:8080/page | ❌ 不同源 | 端口不同 |

同源策略限制了什么?

  • 不能读取不同源的 Cookie、localStorage、IndexedDB
  • 不能获取不同源的 DOM(比如 iframe 内嵌的页面内容)
  • 不能向不同源地址发送 AJAX 请求(XHR 或 fetch)并读取响应

注意:请求是可以发出去的,服务器也收到了请求并返回了响应,但浏览器拒绝将响应交给 JavaScript 代码。这是浏览器的安全拦截,不是网络层面的阻断。

16.4.2 JSONP:利用 script 标签的“漏洞”

在同源策略的限制下,有一个历史悠久的例外:<script> 标签加载外部脚本不受同源策略限制。JSONP(JSON with Padding)正是利用这一特性实现跨域。

原理

  1. 前端动态创建一个 <script> 标签,将其 src 指向目标 API 地址,并附加一个回调函数名作为查询参数。
  2. 服务端接收到请求后,不直接返回 JSON 数据,而是将 JSON 数据包裹在前端指定的回调函数中,返回一段可执行的 JavaScript 代码。
  3. 浏览器加载这个脚本后,立即执行该回调函数,前端通过回调函数拿到数据。

代码示例

// 前端定义回调
function handleData(data) {
  console.log('跨域获取的数据:', data);
}

// 动态创建 script 标签
const script = document.createElement('script');
script.src = 'http://api.example.com/data?jsonp=handleData';
document.body.appendChild(script);

// 后端返回内容示例(不是 JSON,而是 JS 代码)
// handleData({"name": "Alice", "age": 25})

JSONP 的局限

  • 只支持 GET 请求,不支持 POST 等其他方法
  • 需要服务端专门配合,返回格式必须兼容
  • 安全性较差,容易遭受 XSS 攻击(如果对方返回恶意代码也会被执行)
  • 无法处理 HTTP 状态码和错误,异常处理困难

在现代开发中,JSONP 基本已被 CORS 淘汰,但了解它的原理有助于理解浏览器安全策略的演进。

16.4.3 CORS:官方的跨域解决方案

CORS(Cross-Origin Resource Sharing,跨域资源共享) 是 W3C 标准,也是目前解决跨域问题的最佳实践。它通过服务器设置一系列 HTTP 响应头,明确告诉浏览器“这个跨域请求是被允许的”。

简单请求与非简单请求

浏览器将跨域请求分为两类:

简单请求:同时满足以下条件:

  • 请求方法为 GETHEADPOST 之一
  • 请求头只包含 AcceptAccept-LanguageContent-LanguageContent-Type(仅限 text/plainmultipart/form-dataapplication/x-www-form-urlencoded
  • 没有自定义请求头

简单请求直接发送,浏览器会根据响应头中的 Access-Control-Allow-Origin 判断是否允许访问。

非简单请求:凡是不满足上述条件的都是非简单请求(例如发送 application/json 数据、自定义 Header、PUT/DELETE 方法等)。浏览器会先发一个 OPTIONS 预检请求(Preflight),询问服务器是否允许该请求,服务器明确允许后才发送实际请求。

关键响应头

服务端需要设置以下几个 HTTP 响应头:

// 允许的来源,可以是具体域名或 *(通配符)
Access-Control-Allow-Origin: http://localhost:3000

// 允许的请求方法
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS

// 允许的请求头
Access-Control-Allow-Headers: Content-Type, Authorization

// 是否允许携带 Cookie
Access-Control-Allow-Credentials: true

// 预检请求缓存时间(秒),减少重复询问
Access-Control-Max-Age: 86400

注意:当 Access-Control-Allow-Credentials 设置为 true 时,Access-Control-Allow-Origin 不能设为 *,必须指定具体域名。

前端如何配合

前端代码基本不需要额外处理,保持正常的请求写法即可:

// 如果需要带 Cookie,需要设置 credentials
fetch('http://api.example.com/data', {
  credentials: 'include'  // 携带跨域请求的 Cookie
})
.then(res => res.json())
.then(data => console.log(data));

如果使用 axios,全局设置更简单:

axios.defaults.withCredentials = true;

CORS 是标准的、安全的跨域方案,也是前后端分离架构中最常用的方式,推荐优先使用。

16.4.4 代理方案:绕过浏览器的限制

既然跨域限制是浏览器的行为,那么如果请求不经过浏览器,自然就没有跨域问题了。代理(Proxy)正是基于这个思路。

开发环境:webpack-dev-server / Vite 代理

在开发环境中,webpack-dev-server 和 Vite 都提供了代理配置,将特定路径的请求转发到后端服务器。浏览器以为是同源请求,实际上被开发服务器转发到了后端。

Vite 配置示例vite.config.js):

export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
}

前端请求 /api/users,Vite 开发服务器将其代理转发到 http://localhost:8080/users,响应原路返回。浏览器只与开发服务器通信,完美规避跨域。

生产环境:Nginx 反向代理

在真正部署上线后,通常使用 Nginx 作为反向代理服务器,将前端静态资源和后端 API 统一到同一个域名下。

Nginx 配置示例:

server {
    listen 80;
    server_name www.example.com;

    # 前端静态资源
    location / {
        root /var/www/frontend/dist;
        index index.html;
        try_files $uri $uri/ /index.html;
    }

    # 后端 API 反向代理
    location /api/ {
        proxy_pass http://api-server:8080/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

这样,前端页面和 API 接口都在 www.example.com 域名下,完全同源,无需 CORS 配置,也更安全(API 服务器可以不对公网暴露)。

16.4.5 postMessage:窗口与 iframe 间的跨域通信

有时需要解决的不是 AJAX 请求的跨域,而是两个不同源的窗口(或 iframe)之间的通信。window.postMessage 是浏览器提供的安全跨域通信 API。

使用场景

  • 父页面与嵌入的跨域 iframe 通信
  • 两个打开的浏览器标签页之间通信

发送消息

// 父页面向 iframe 发送消息
const iframe = document.querySelector('iframe');
iframe.contentWindow.postMessage('Hello from parent', 'http://child.example.com');

// 子页面监听消息
window.addEventListener('message', (event) => {
  // 验证来源域名,防止恶意消息
  if (event.origin !== 'http://parent.example.com') return;
  console.log('收到的消息:', event.data);
  // 可以回复消息
  event.source.postMessage('Got it!', event.origin);
});

postMessage 通信是双向的,但需要双方都验证 event.origin,不能盲目信任,否则会有安全风险。

16.4.6 其他方案简述

除了上述主流方案外,还有一些较少使用的跨域手段,简单了解即可:

  • WebSocket:不受同源策略限制(但建立连接时的握手是通过 HTTP 完成的,仍需处理跨域),之后就可以自由双向通信。
  • window.name:利用 window.name 属性在不同页面跳转时依然保持不变的特性传输数据,是一种 hack 方案,现在已不推荐。
  • document.domain:将两个页面的 document.domain 设置为相同主域来通信,只能用于主域相同、子域不同的情况,且安全性有限,已逐渐被废弃。

16.4.7 方案选型建议

在实际开发中,推荐优先级如下:

  1. CORS:前后端都可以控制,标准化且安全,是首选方案。
  2. 开发代理:开发阶段使用 Vite/Webpack 代理,零配置跨域。
  3. Nginx 反向代理:生产环境统一域名,无需前端操心。
  4. postMessage:用于 iframe 或窗口间通信,而非 AJAX 跨域。
  5. JSONP:除非需要兼容非常老旧的浏览器,否则不推荐使用。

理解跨域问题的本质——它不是网络障碍,而是浏览器对用户数据安全的保护——之后,选择合适的解决方案就能从容应对。