【前端+跨域】跨域问题全面解析与解决方案指南

跨域问题全面解析与解决方案指南

📖 目录


前言

前后端分离微服务架构 盛行的今天,跨域(Cross-Origin) 已成为每位Web开发者必须面对的核心问题。它源于浏览器的同源策略(Same-Origin Policy) ------一项至关重要的安全机制,旨在防止恶意网站窃取用户数据。然而,这项安全机制也给合法的前后端通信带来了挑战。

本文旨在为你提供一份全面、实用 的跨域问题解决指南。无论你是刚接触跨域概念 的新手,还是需要为复杂项目选择合适方案的资深开发者,都能在这里找到清晰的解释、可运行的代码示例 以及基于实际场景的选型建议 。我们将从基础概念 入手,逐步深入8种主流解决方案 ,助你彻底掌握跨域问题的应对之道。

摘要

本文系统性地解析跨域问题 的本质与解决方案。首先阐述同源策略 的原理及其必要性,通过流程图 直观展示跨域请求的完整处理链路。随后深入讲解8种主流跨域方案

  1. CORS(跨域资源共享) - W3C标准 ,现代Web开发首选
  2. 代理服务器 - 开发环境 常用,生产环境 通过Nginx等实现
  3. JSONP - 兼容性极佳 但仅限GET请求
  4. postMessage - 窗口/iframe间安全通信
  5. WebSocket - 实时双向通信协议
  6. document.domain + iframe - 同主域不同子域场景
  7. window.name + iframe - 兼容老旧浏览器的备选方案
  8. 服务器端转发 - 调用第三方API的通用方案

每种方案均包含实现原理代码示例优缺点分析适用场景 。文章最后提供详细的对比表格选型指南 ,帮助你在不同项目背景 下做出明智的技术决策

什么是跨域?

跨域(Cross-Origin)是指浏览器出于安全考虑,对网页中发起的网络请求施加的同源策略限制。具体来说,当当前页面中的某个接口请求的地址和当前页面的地址,如果协议、域名、端口其中有一项不同,浏览器就会认为该请求跨域了。

同源策略(Same-Origin Policy)

同源策略是浏览器最基本的安全策略之一,它限制了一个源(origin)的文档或脚本如何与另一个源的资源进行交互。这个策略可以防止恶意网站窃取用户数据。

同源的定义 :两个URL的协议(protocol)、域名(host)、端口(port) 必须完全相同。

对比项 是否同源 说明
https://www.example.com/index.htmlhttps://www.example.com/api/user ✅ 同源 协议、域名、端口完全相同
http://www.example.comhttps://www.example.com ❌ 跨域 协议不同(http vs https)
https://www.example.comhttps://api.example.com ❌ 跨域 域名不同(主域 vs 子域)
https://www.example.com:80https://www.example.com:8080 ❌ 跨域 端口不同(80 vs 8080)

为什么需要跨域限制?

浏览器实施同源策略的主要原因:

  1. 保护用户隐私:防止恶意网站读取其他网站的Cookie、LocalStorage等敏感数据
  2. 防止CSRF攻击:限制恶意网站伪造用户身份发起请求
  3. 隔离恶意文档:防止恶意脚本操纵其他网站的DOM

跨域错误信息

当发生跨域请求时,浏览器会阻止请求并返回类似以下错误:

复制代码
Access to fetch at 'https://api.other-site.com/data' from origin 'https://www.my-site.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

跨域请求流程与解决方案概览

为了更好地理解跨域问题的产生和解决思路,下面通过流程图展示从浏览器发起跨域请求到被阻止,再到通过各种方案解决的完整逻辑链路:
#mermaid-svg-dnnTKr82v1Q1IwsR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dnnTKr82v1Q1IwsR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dnnTKr82v1Q1IwsR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dnnTKr82v1Q1IwsR .error-icon{fill:#552222;}#mermaid-svg-dnnTKr82v1Q1IwsR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dnnTKr82v1Q1IwsR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dnnTKr82v1Q1IwsR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dnnTKr82v1Q1IwsR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dnnTKr82v1Q1IwsR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dnnTKr82v1Q1IwsR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dnnTKr82v1Q1IwsR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dnnTKr82v1Q1IwsR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dnnTKr82v1Q1IwsR .marker.cross{stroke:#333333;}#mermaid-svg-dnnTKr82v1Q1IwsR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dnnTKr82v1Q1IwsR p{margin:0;}#mermaid-svg-dnnTKr82v1Q1IwsR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dnnTKr82v1Q1IwsR .cluster-label text{fill:#333;}#mermaid-svg-dnnTKr82v1Q1IwsR .cluster-label span{color:#333;}#mermaid-svg-dnnTKr82v1Q1IwsR .cluster-label span p{background-color:transparent;}#mermaid-svg-dnnTKr82v1Q1IwsR .label text,#mermaid-svg-dnnTKr82v1Q1IwsR span{fill:#333;color:#333;}#mermaid-svg-dnnTKr82v1Q1IwsR .node rect,#mermaid-svg-dnnTKr82v1Q1IwsR .node circle,#mermaid-svg-dnnTKr82v1Q1IwsR .node ellipse,#mermaid-svg-dnnTKr82v1Q1IwsR .node polygon,#mermaid-svg-dnnTKr82v1Q1IwsR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dnnTKr82v1Q1IwsR .rough-node .label text,#mermaid-svg-dnnTKr82v1Q1IwsR .node .label text,#mermaid-svg-dnnTKr82v1Q1IwsR .image-shape .label,#mermaid-svg-dnnTKr82v1Q1IwsR .icon-shape .label{text-anchor:middle;}#mermaid-svg-dnnTKr82v1Q1IwsR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dnnTKr82v1Q1IwsR .rough-node .label,#mermaid-svg-dnnTKr82v1Q1IwsR .node .label,#mermaid-svg-dnnTKr82v1Q1IwsR .image-shape .label,#mermaid-svg-dnnTKr82v1Q1IwsR .icon-shape .label{text-align:center;}#mermaid-svg-dnnTKr82v1Q1IwsR .node.clickable{cursor:pointer;}#mermaid-svg-dnnTKr82v1Q1IwsR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dnnTKr82v1Q1IwsR .arrowheadPath{fill:#333333;}#mermaid-svg-dnnTKr82v1Q1IwsR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dnnTKr82v1Q1IwsR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dnnTKr82v1Q1IwsR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dnnTKr82v1Q1IwsR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dnnTKr82v1Q1IwsR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dnnTKr82v1Q1IwsR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dnnTKr82v1Q1IwsR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dnnTKr82v1Q1IwsR .cluster text{fill:#333;}#mermaid-svg-dnnTKr82v1Q1IwsR .cluster span{color:#333;}#mermaid-svg-dnnTKr82v1Q1IwsR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dnnTKr82v1Q1IwsR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dnnTKr82v1Q1IwsR rect.text{fill:none;stroke-width:0;}#mermaid-svg-dnnTKr82v1Q1IwsR .icon-shape,#mermaid-svg-dnnTKr82v1Q1IwsR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dnnTKr82v1Q1IwsR .icon-shape p,#mermaid-svg-dnnTKr82v1Q1IwsR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dnnTKr82v1Q1IwsR .icon-shape .label rect,#mermaid-svg-dnnTKr82v1Q1IwsR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dnnTKr82v1Q1IwsR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dnnTKr82v1Q1IwsR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dnnTKr82v1Q1IwsR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是



浏览器发起跨域请求
请求是否同源?
正常请求/响应
浏览器检查CORS响应头
服务器是否允许该源?
请求成功,返回数据
浏览器阻止请求,抛出CORS错误
开发者选择解决方案
CORS方案
服务端配置响应头

Access-Control-Allow-Origin
浏览器收到允许头,放行请求
代理服务器方案
请求发送到同源代理
代理转发到目标服务器
代理返回响应给浏览器
JSONP方案
动态创建script标签
script加载跨域JS文件
执行回调函数获取数据
其他方案
postMessage

(窗口间通信)
WebSocket

(实时双向通信)
服务器端转发

(服务端代理)
成功获取数据

流程图说明

  1. 跨域检测阶段:浏览器发起请求时,会检查目标URL与当前页面的源是否一致
  2. CORS检查阶段:如果跨域,浏览器会检查服务器返回的CORS响应头
  3. 解决方案选择:当请求被阻止时,开发者需要选择合适的跨域解决方案
  4. 方案执行路径
    • CORS:服务端配置响应头,浏览器自动处理
    • 代理服务器:通过同源代理中转请求
    • JSONP:利用script标签无跨域限制的特性
    • 其他方案:根据具体场景选择合适的技术

跨域的常见场景

  1. 前后端分离开发 :前端运行在 http://localhost:3000,后端API在 http://localhost:8080
  2. 调用第三方API:自己的网站需要调用其他服务商的API接口
  3. 微服务架构:不同服务部署在不同域名或端口下
  4. CDN资源引用:从不同域的CDN加载静态资源

8种跨域解决方案详解

1. CORS(跨域资源共享)⭐️ 最常用

CORS(Cross-Origin Resource Sharing)是W3C标准,是目前最主流的跨域解决方案。它允许服务器声明哪些源可以访问资源。

简单请求 vs 预检请求

简单请求(Simple Request)条件:

  • 方法为:GET、HEAD、POST
  • Content-Type为:text/plainmultipart/form-dataapplication/x-www-form-urlencoded

预检请求(Preflight Request):不满足简单请求条件的请求,浏览器会先发送OPTIONS请求进行预检。

服务端CORS配置示例

Node.js Express示例:

javascript 复制代码
const express = require('express');
const app = express();

// 允许所有源访问(生产环境应指定具体域名)
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.setHeader('Access-Control-Allow-Credentials', 'true'); // 允许携带Cookie
  res.setHeader('Access-Control-Max-Age', '86400'); // 预检请求缓存时间(秒)
  
  if (req.method === 'OPTIONS') {
    return res.sendStatus(200);
  }
  next();
});

// 或者使用cors中间件
const cors = require('cors');
app.use(cors({
  origin: 'https://www.example.com', // 指定允许的源
  credentials: true, // 允许携带凭证
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

app.get('/api/data', (req, res) => {
  res.json({ message: 'CORS enabled!' });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Spring Boot示例:

java 复制代码
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("https://www.example.com")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

// 或者在Controller方法上使用注解
@RestController
@RequestMapping("/api")
public class ApiController {
    
    @CrossOrigin(origins = "https://www.example.com")
    @GetMapping("/data")
    public ResponseEntity<?> getData() {
        return ResponseEntity.ok("CORS enabled!");
    }
}

2. 代理服务器(Node中间件/Nginx反向代理)

代理服务器架构示意图

代理服务器方案的核心是通过同源代理中转请求,以下是其数据流向示意图:
#mermaid-svg-Jr77pjUhRyw2u60K{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Jr77pjUhRyw2u60K .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Jr77pjUhRyw2u60K .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Jr77pjUhRyw2u60K .error-icon{fill:#552222;}#mermaid-svg-Jr77pjUhRyw2u60K .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Jr77pjUhRyw2u60K .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Jr77pjUhRyw2u60K .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Jr77pjUhRyw2u60K .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Jr77pjUhRyw2u60K .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Jr77pjUhRyw2u60K .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Jr77pjUhRyw2u60K .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Jr77pjUhRyw2u60K .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Jr77pjUhRyw2u60K .marker.cross{stroke:#333333;}#mermaid-svg-Jr77pjUhRyw2u60K svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Jr77pjUhRyw2u60K p{margin:0;}#mermaid-svg-Jr77pjUhRyw2u60K .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Jr77pjUhRyw2u60K .cluster-label text{fill:#333;}#mermaid-svg-Jr77pjUhRyw2u60K .cluster-label span{color:#333;}#mermaid-svg-Jr77pjUhRyw2u60K .cluster-label span p{background-color:transparent;}#mermaid-svg-Jr77pjUhRyw2u60K .label text,#mermaid-svg-Jr77pjUhRyw2u60K span{fill:#333;color:#333;}#mermaid-svg-Jr77pjUhRyw2u60K .node rect,#mermaid-svg-Jr77pjUhRyw2u60K .node circle,#mermaid-svg-Jr77pjUhRyw2u60K .node ellipse,#mermaid-svg-Jr77pjUhRyw2u60K .node polygon,#mermaid-svg-Jr77pjUhRyw2u60K .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Jr77pjUhRyw2u60K .rough-node .label text,#mermaid-svg-Jr77pjUhRyw2u60K .node .label text,#mermaid-svg-Jr77pjUhRyw2u60K .image-shape .label,#mermaid-svg-Jr77pjUhRyw2u60K .icon-shape .label{text-anchor:middle;}#mermaid-svg-Jr77pjUhRyw2u60K .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Jr77pjUhRyw2u60K .rough-node .label,#mermaid-svg-Jr77pjUhRyw2u60K .node .label,#mermaid-svg-Jr77pjUhRyw2u60K .image-shape .label,#mermaid-svg-Jr77pjUhRyw2u60K .icon-shape .label{text-align:center;}#mermaid-svg-Jr77pjUhRyw2u60K .node.clickable{cursor:pointer;}#mermaid-svg-Jr77pjUhRyw2u60K .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Jr77pjUhRyw2u60K .arrowheadPath{fill:#333333;}#mermaid-svg-Jr77pjUhRyw2u60K .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Jr77pjUhRyw2u60K .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Jr77pjUhRyw2u60K .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Jr77pjUhRyw2u60K .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Jr77pjUhRyw2u60K .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Jr77pjUhRyw2u60K .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Jr77pjUhRyw2u60K .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Jr77pjUhRyw2u60K .cluster text{fill:#333;}#mermaid-svg-Jr77pjUhRyw2u60K .cluster span{color:#333;}#mermaid-svg-Jr77pjUhRyw2u60K div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Jr77pjUhRyw2u60K .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Jr77pjUhRyw2u60K rect.text{fill:none;stroke-width:0;}#mermaid-svg-Jr77pjUhRyw2u60K .icon-shape,#mermaid-svg-Jr77pjUhRyw2u60K .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Jr77pjUhRyw2u60K .icon-shape p,#mermaid-svg-Jr77pjUhRyw2u60K .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Jr77pjUhRyw2u60K .icon-shape .label rect,#mermaid-svg-Jr77pjUhRyw2u60K .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Jr77pjUhRyw2u60K .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Jr77pjUhRyw2u60K .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Jr77pjUhRyw2u60K :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 目标服务器
代理服务器
浏览器端

  1. 发起请求

/api/data
2. 接收请求
3. 转发请求
4. 返回数据
5. 返回响应
6. 返回给浏览器
前端应用

https://www.my-site.com
Nginx/Node.js代理

https://www.my-site.com
请求转发模块
后端API服务

https://api.target.com

数据流向说明:

  1. 浏览器向同源代理服务器发起请求(https://www.my-site.com/api/data
  2. 代理服务器接收请求并解析
  3. 代理服务器将请求转发到目标服务器(https://api.target.com/data
  4. 目标服务器处理请求并返回数据
  5. 代理服务器接收目标服务器的响应
  6. 代理服务器将响应返回给浏览器

关键优势:

  • 浏览器认为所有请求都是同源的,完全绕过跨域限制
  • 可以在代理层统一添加认证、日志、缓存等功能
  • 前端代码无需任何修改,透明无感知

主页面(发送方):

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>主页面</title>
</head>
<body>
    <iframe id="iframe" src="http://other-domain.com/child.html"></iframe>
    
    <script>
        const iframe = document.getElementById('iframe');
        
        // 等待iframe加载完成
        iframe.onload = function() {
            // 向iframe发送消息
            iframe.contentWindow.postMessage({
                type: 'greeting',
                message: 'Hello from parent!'
            }, 'http://other-domain.com');
        };
        
        // 接收来自iframe的消息
        window.addEventListener('message', function(event) {
            // 验证消息来源
            if (event.origin !== 'http://other-domain.com') {
                return;
            }
            
            console.log('收到消息:', event.data);
            
            if (event.data.type === 'response') {
                document.getElementById('result').innerHTML = event.data.message;
            }
        });
    </script>
    
    <div id="result"></div>
</body>
</html>

子页面(接收方):

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>子页面</title>
</head>
<body>
    <script>
        // 接收来自父页面的消息
        window.addEventListener('message', function(event) {
            // 验证消息来源
            if (event.origin !== 'http://parent-domain.com') {
                return;
            }
            
            console.log('收到父页面消息:', event.data);
            
            if (event.data.type === 'greeting') {
                // 向父页面回复
                event.source.postMessage({
                    type: 'response',
                    message: 'Hello back from child!'
                }, event.origin);
            }
        });
    </script>
</body>
</html>

5. WebSocket

WebSocket协议本身支持跨域,可以在建立连接时指定允许的源。

前端实现:

javascript 复制代码
const socket = new WebSocket('ws://api.example.com/ws');

socket.onopen = function() {
    console.log('WebSocket连接已建立');
    socket.send(JSON.stringify({ type: 'subscribe', channel: 'updates' }));
};

socket.onmessage = function(event) {
    const data = JSON.parse(event.data);
    console.log('收到消息:', data);
};

socket.onerror = function(error) {
    console.error('WebSocket错误:', error);
};

socket.onclose = function() {
    console.log('WebSocket连接已关闭');
};

后端实现(Node.js + ws库):

javascript 复制代码
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', function connection(ws, req) {
    const origin = req.headers.origin;
    
    // 可以在这里验证origin
    const allowedOrigins = ['https://www.example.com', 'https://app.example.com'];
    
    if (!allowedOrigins.includes(origin)) {
        ws.close();
        return;
    }
    
    ws.on('message', function incoming(message) {
        console.log('收到消息:', message);
        
        // 广播给所有客户端
        wss.clients.forEach(function each(client) {
            if (client.readyState === WebSocket.OPEN) {
                client.send(JSON.stringify({ 
                    type: 'broadcast', 
                    data: message 
                }));
            }
        });
    });
    
    ws.send(JSON.stringify({ type: 'welcome', message: '连接成功' }));
});

6. document.domain + iframe

适用于主域相同、子域不同的情况。

主页面(www.example.com):

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>主页面</title>
</head>
<body>
    <iframe id="iframe" src="http://api.example.com/iframe.html"></iframe>
    
    <script>
        // 设置相同的document.domain
        document.domain = 'example.com';
        
        const iframe = document.getElementById('iframe');
        
        iframe.onload = function() {
            // 现在可以访问iframe的内容了
            const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
            console.log('iframe内容:', iframeDoc.body.innerHTML);
            
            // 也可以调用iframe中的函数
            iframe.contentWindow.someFunction();
        };
    </script>
</body>
</html>

iframe页面(api.example.com):

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>iframe页面</title>
</head>
<body>
    <script>
        // 设置相同的document.domain
        document.domain = 'example.com';
        
        // 定义可以被父页面调用的函数
        window.someFunction = function() {
            console.log('iframe中的函数被调用');
            return 'Hello from iframe!';
        };
        
        // 也可以访问父页面的内容
        console.log('父页面标题:', window.parent.document.title);
    </script>
</body>
</html>

7. window.name + iframe

利用window.name在不同页面间传递数据,window.name在不同页面加载后依然存在。

实现原理:

  1. 在iframe中加载目标页面,目标页面将数据赋值给window.name
  2. 将iframe的src改为同源的空页面
  3. 在同源的空页面中,父页面可以读取iframe的window.name

前端实现示例:

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>window.name跨域示例</title>
</head>
<body>
    <div id="result">等待数据...</div>
    
    <script>
        // 创建iframe
        const iframe = document.createElement('iframe');
        iframe.style.display = 'none';
        document.body.appendChild(iframe);
        
        let state = 0; // 状态:0-初始,1-加载目标页面,2-加载同源页面
        
        // 监听iframe的onload事件
        iframe.onload = function() {
            if (state === 0) {
                // 第一步:加载目标页面(跨域)
                iframe.contentWindow.location = 'http://api.example.com/data.html';
                state = 1;
            } else if (state === 1) {
                // 第二步:加载同源的空页面
                iframe.contentWindow.location = 'about:blank';
                state = 2;
            } else if (state === 2) {
                // 第三步:读取window.name
                try {
                    const data = iframe.contentWindow.name;
                    if (data) {
                        const result = JSON.parse(data);
                        document.getElementById('result').innerHTML = 
                            `用户: ${result.name}, 年龄: ${result.age}`;
                        console.log('获取到的数据:', result);
                    }
                } catch (error) {
                    console.error('解析数据失败:', error);
                }
                
                // 清理iframe
                document.body.removeChild(iframe);
            }
        };
        
        // 开始跨域请求
        iframe.src = 'http://api.example.com/data.html';
    </script>
</body>
</html>

目标页面(api.example.com/data.html):

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>数据页面</title>
</head>
<body>
    <script>
        // 将数据存储在window.name中
        const data = {
            name: '张三',
            age: 25,
            email: 'zhangsan@example.com',
            timestamp: Date.now()
        };
        window.name = JSON.stringify(data);
    </script>
</body>
</html>

技术要点:

  1. window.name特性:window.name属性在不同页面加载后依然保持,且容量较大(约2MB)
  2. 同源策略绕过通过先加载跨域页面设置window.name再加载同源页面读取window.name
  3. 状态管理:需要维护加载状态,确保在正确时机读取数据

优点:

  1. 兼容性好:支持IE6+等老版本浏览器
  2. 数据传输量大:window.name可存储约2MB数据
  3. 相对安全:相比JSONP,不易受到XSS攻击

缺点:

  1. 实现复杂:需要维护状态机和多次页面加载
  2. 性能较差:需要加载两次页面,影响性能
  3. 只能单向传递:只能从目标页面向父页面传递数据
  4. 现代替代方案:已被postMessage等更现代的API取代

适用场景:

  1. 需要兼容IE6-IE8的老旧系统
  2. 需要传递较大数据量的跨域通信
  3. 目标服务器不支持CORS且无法修改的情况

注意事项:

  1. 确保同源的空页面存在或使用about:blank
  2. 注意处理JSON解析错误
  3. 及时清理iframe,避免内存泄漏
  4. 现代项目中建议优先使用CORS或postMessage

8. 服务器端转发

服务器端转发架构示意图

服务器端转发方案完全由服务端处理跨域请求,前端只与自己的服务器通信:
#mermaid-svg-zzO9YXATLv7kQ9eT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zzO9YXATLv7kQ9eT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zzO9YXATLv7kQ9eT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zzO9YXATLv7kQ9eT .error-icon{fill:#552222;}#mermaid-svg-zzO9YXATLv7kQ9eT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zzO9YXATLv7kQ9eT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zzO9YXATLv7kQ9eT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zzO9YXATLv7kQ9eT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zzO9YXATLv7kQ9eT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zzO9YXATLv7kQ9eT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zzO9YXATLv7kQ9eT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zzO9YXATLv7kQ9eT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zzO9YXATLv7kQ9eT .marker.cross{stroke:#333333;}#mermaid-svg-zzO9YXATLv7kQ9eT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zzO9YXATLv7kQ9eT p{margin:0;}#mermaid-svg-zzO9YXATLv7kQ9eT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zzO9YXATLv7kQ9eT .cluster-label text{fill:#333;}#mermaid-svg-zzO9YXATLv7kQ9eT .cluster-label span{color:#333;}#mermaid-svg-zzO9YXATLv7kQ9eT .cluster-label span p{background-color:transparent;}#mermaid-svg-zzO9YXATLv7kQ9eT .label text,#mermaid-svg-zzO9YXATLv7kQ9eT span{fill:#333;color:#333;}#mermaid-svg-zzO9YXATLv7kQ9eT .node rect,#mermaid-svg-zzO9YXATLv7kQ9eT .node circle,#mermaid-svg-zzO9YXATLv7kQ9eT .node ellipse,#mermaid-svg-zzO9YXATLv7kQ9eT .node polygon,#mermaid-svg-zzO9YXATLv7kQ9eT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zzO9YXATLv7kQ9eT .rough-node .label text,#mermaid-svg-zzO9YXATLv7kQ9eT .node .label text,#mermaid-svg-zzO9YXATLv7kQ9eT .image-shape .label,#mermaid-svg-zzO9YXATLv7kQ9eT .icon-shape .label{text-anchor:middle;}#mermaid-svg-zzO9YXATLv7kQ9eT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zzO9YXATLv7kQ9eT .rough-node .label,#mermaid-svg-zzO9YXATLv7kQ9eT .node .label,#mermaid-svg-zzO9YXATLv7kQ9eT .image-shape .label,#mermaid-svg-zzO9YXATLv7kQ9eT .icon-shape .label{text-align:center;}#mermaid-svg-zzO9YXATLv7kQ9eT .node.clickable{cursor:pointer;}#mermaid-svg-zzO9YXATLv7kQ9eT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zzO9YXATLv7kQ9eT .arrowheadPath{fill:#333333;}#mermaid-svg-zzO9YXATLv7kQ9eT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zzO9YXATLv7kQ9eT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zzO9YXATLv7kQ9eT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zzO9YXATLv7kQ9eT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zzO9YXATLv7kQ9eT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zzO9YXATLv7kQ9eT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zzO9YXATLv7kQ9eT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zzO9YXATLv7kQ9eT .cluster text{fill:#333;}#mermaid-svg-zzO9YXATLv7kQ9eT .cluster span{color:#333;}#mermaid-svg-zzO9YXATLv7kQ9eT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zzO9YXATLv7kQ9eT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zzO9YXATLv7kQ9eT rect.text{fill:none;stroke-width:0;}#mermaid-svg-zzO9YXATLv7kQ9eT .icon-shape,#mermaid-svg-zzO9YXATLv7kQ9eT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zzO9YXATLv7kQ9eT .icon-shape p,#mermaid-svg-zzO9YXATLv7kQ9eT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zzO9YXATLv7kQ9eT .icon-shape .label rect,#mermaid-svg-zzO9YXATLv7kQ9eT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zzO9YXATLv7kQ9eT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zzO9YXATLv7kQ9eT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zzO9YXATLv7kQ9eT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 第三方API
自有服务器
浏览器端

  1. 请求自有服务器

/proxy/api?param=value
2. 服务器接收请求
3. 添加认证头等

转发到第三方API
4. 返回数据
5. 处理响应数据
6. 返回标准化数据
前端应用

https://www.my-site.com
Node.js/Java/Python服务

https://www.my-site.com
请求转发模块
目标API服务

https://api.third-party.com

数据流向说明:

  1. 浏览器向自己的服务器发起请求(https://www.my-site.com/proxy/api
  2. 自有服务器接收请求并验证权限
  3. 服务器添加必要的认证头,转发请求到第三方API
  4. 第三方API处理请求并返回数据
  5. 自有服务器接收响应,可进行数据清洗、转换等处理
  6. 自有服务器将处理后的数据返回给浏览器

适用场景:

  • 调用不支持CORS的第三方API
  • 需要统一添加认证信息的场景
  • 数据聚合或转换需求
  • 隐藏真实API地址,增强安全性

注意事项:

  • 增加了服务器负载和网络延迟
  • 需要确保不违反第三方API的使用条款
  • 要做好错误处理和超时控制

完全在服务器端完成跨域请求,前端只请求自己的服务器。

Node.js示例:

javascript 复制代码
const express = require('express');
const axios = require('axios');
const app = express();

app.get('/proxy/api', async (req, res) => {
    try {
        // 从自己的服务器发起请求到目标API
        const response = await axios.get('https://api.target.com/data', {
            params: req.query, // 传递前端参数
            headers: {
                // 可以在这里添加认证头等
                'Authorization': 'Bearer your-token'
            }
        });
        
        // 将结果返回给前端
        res.json(response.data);
    } catch (error) {
        console.error('代理请求失败:', error);
        res.status(500).json({ error: '代理请求失败' });
    }
});

app.listen(3000, () => {
    console.log('代理服务器运行在 http://localhost:3000');
});

跨域解决方案对比

方案 优点 缺点 适用场景
CORS 1. W3C标准方案,浏览器原生支持 2. 安全性好,支持细粒度控制 3. 支持所有HTTP方法(GET、POST、PUT、DELETE等) 4. 前端无需额外代码,配置简单 1. 需要后端服务器配合配置响应头 2. 旧浏览器(IE9及以下)兼容性有限 3. 复杂请求会触发预检(OPTIONS),增加一次请求开销 4. 携带Cookie时不能使用通配符*,必须指定具体源 1. 前后端分离的现代Web应用 2. RESTful API服务 3. 可控的后端服务(能修改服务器配置) 4. 需要支持多种HTTP方法的场景
代理服务器 1. 前端代码无需任何修改,完全透明 2. 支持所有请求类型和HTTP方法 3. 可在代理层统一添加认证、日志、缓存、限流等功能 4. 完全绕过浏览器同源限制,兼容所有浏览器 1. 需要额外服务器配置和维护 2. 增加网络跳转,可能带来额外延迟 3. 存在单点故障风险(代理服务器宕机影响所有请求) 4. 增加系统复杂度,需要监控代理服务器性能 1. 开发环境本地代理(Webpack/Vite devServer) 2. 生产环境Nginx反向代理部署 3. 微服务架构中的API网关 4. 需要统一管理多个后端服务的场景
JSONP 1. 兼容性极好,支持IE6+等所有老版本浏览器 2. 实现简单,无需复杂配置 3. 无需服务器特殊支持(标准JSONP格式即可) 4. 可绕过CORS限制访问不支持CORS的第三方API 1. 仅支持GET请求,不支持POST、PUT等其他HTTP方法 2. 安全性差,容易受到XSS攻击 3. 错误处理困难,难以获取HTTP状态码 4. 需要后端配合返回特定格式(函数调用包装) 5. 只能被动接收数据,无法主动发送复杂请求 1. 需要兼容IE8及以下浏览器的老项目 2. 简单的数据获取场景(仅需GET请求) 3. 调用不支持CORS的第三方公开API 4. 临时解决方案或原型开发
postMessage 1. 安全可控,可验证消息来源(origin验证) 2. 支持双向通信,父子页面可互相发送消息 3. 不受同源策略限制,可跨任意域通信 4. 支持传输复杂数据结构(对象、数组等) 5. HTML5标准API,现代浏览器支持良好 1. 只能用于窗口/iframe间通信,不适用于普通API请求 2. 实现相对复杂,需要双方页面配合 3. 需要建立消息监听机制,代码结构较复杂 4. 不适合大量频繁的数据传输 1. 跨域iframe通信(如第三方嵌入组件) 2. 微前端架构中的子应用通信 3. 多标签页应用数据同步 4. 父子窗口间的安全数据传递
WebSocket 1. 真正的实时双向通信,支持服务器主动推送 2. 协议级别支持跨域,连接建立后可自由通信 3. 相比HTTP轮询,减少网络开销和延迟 4. 支持二进制和文本数据格式 5. 连接持久化,避免重复握手开销 1. 需要服务端支持WebSocket协议 2. 连接保持需要额外资源开销 3. 比HTTP更复杂,需要处理连接状态 4. 部分老旧代理服务器可能不支持WebSocket 5. 需要额外的心跳机制维持连接 1. 实时聊天应用 2. 股票行情、实时报价系统 3. 在线协作工具(如文档协同编辑) 4. 多人在线游戏 5. 实时监控和仪表盘
document.domain 1. 解决同主域下不同子域的跨域问题 2. 无需服务器配置,纯前端解决方案 3. 兼容性较好(IE8+) 4. 设置后可直接访问对方DOM和JavaScript对象 1. 仅限同一主域下的不同子域(如a.example.com和b.example.com) 2. 只能设置到主域,不能设置到其他域 3. 安全性需注意,设置后子域间完全信任 4. 现代浏览器中逐渐被CORS替代 1. 同一公司不同子域系统集成 2. 历史遗留系统,无法修改服务器配置 3. 需要直接操作对方DOM的特殊场景 4. 简单的子域间数据共享
window.name 1. 兼容性极好,支持IE6+等所有老浏览器 2. 可传递较大数据量(约2MB) 3. 相对JSONP更安全,不易受到XSS攻击 4. 不需要服务器特殊支持 1. 实现复杂,需要维护状态机和多次页面加载 2. 性能较差,需要加载两次页面(跨域页+同源页) 3. 只能单向传递数据(从目标页面到父页面) 4. 现代方案已较少使用,有更好的替代方案 5. 代码可读性和维护性差 1. 需要兼容IE6-IE8的极端老旧系统 2. 需要传递较大数据量的跨域通信 3. 目标服务器不支持CORS且无法修改的情况 4. 作为其他方案不可用时的备选方案
服务器端转发 1. 前端完全无跨域问题,所有请求都是同源 2. 可统一添加认证、签名、缓存等逻辑 3. 隐藏真实API地址,增强安全性 4. 可对第三方API响应进行数据清洗和转换 5. 支持所有HTTP方法和请求类型 1. 增加服务器负载,所有请求都经过自有服务器中转 2. 单点故障风险(自有服务器宕机影响所有服务) 3. 可能违反第三方API的使用条款 4. 增加网络延迟(多一次跳转) 5. 需要处理错误传递和超时控制 1. 调用不支持CORS的第三方API 2. 需要统一添加认证信息的场景 3. 数据聚合服务(聚合多个API数据) 4. 需要隐藏真实后端地址的安全敏感场景 5. 对第三方API响应需要额外处理的场景

实际开发中的选择建议

开发环境

  1. 本地开发:使用Webpack/Vite的代理配置
  2. 联调测试:后端配置CORS允许测试域名

生产环境

  1. 前后端同域:部署到同一域名下,避免跨域问题
  2. 前后端分离
    • 推荐:Nginx反向代理 + CORS
    • 备选:API网关统一处理跨域
  3. 第三方API调用
    • 可控的API:要求对方支持CORS
    • 不可控的API:使用自己的服务器转发

安全注意事项

  1. CORS配置不要滥用*:生产环境应指定具体的允许来源
  2. 验证Origin头:防止伪造Origin的攻击
  3. 使用HTTPS:确保数据传输安全
  4. 限制允许的HTTP方法:只开放必要的方法
  5. 设置合理的缓存时间:避免频繁的预检请求

常见问题与调试技巧

1. 预检请求失败

javascript 复制代码
// 确保OPTIONS请求返回正确的CORS头
app.options('*', (req, res) => {
    res.setHeader('Access-Control-Allow-Origin', 'https://www.example.com');
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.sendStatus(200);
});

2. 携带Cookie的跨域请求

当需要发送携带Cookie或其他凭证(如HTTP认证信息)的跨域请求时,需要在前后端都进行特殊配置。

前端配置
javascript 复制代码
// Fetch API
fetch('https://api.example.com/data', {
    method: 'GET',
    credentials: 'include', // 关键:携带Cookie
    headers: {
        'Content-Type': 'application/json'
    }
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

// Axios
axios.get('https://api.example.com/data', {
    withCredentials: true // 关键:携带Cookie
})
.then(response => console.log(response.data))
.catch(error => console.error('Error:', error));

// XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.withCredentials = true; // 关键:携带Cookie
xhr.open('GET', 'https://api.example.com/data');
xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(JSON.parse(xhr.responseText));
    }
};
xhr.send();
后端配置
javascript 复制代码
// Node.js Express
const express = require('express');
const app = express();

app.use((req, res, next) => {
    // 不能使用通配符*,必须指定具体的源
    res.setHeader('Access-Control-Allow-Origin', 'https://www.example.com');
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.setHeader('Access-Control-Allow-Credentials', 'true'); // 关键:允许携带凭证
    res.setHeader('Access-Control-Max-Age', '86400');
    
    if (req.method === 'OPTIONS') {
        return res.sendStatus(200);
    }
    next();
});

// 或者使用cors中间件
const cors = require('cors');
app.use(cors({
    origin: 'https://www.example.com', // 必须指定具体源,不能是*
    credentials: true, // 关键:允许携带凭证
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization']
}));
Spring Boot配置
java 复制代码
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("https://www.example.com") // 必须指定具体源
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*")
                .allowCredentials(true) // 关键:允许携带凭证
                .maxAge(3600);
    }
}
注意事项
  1. Access-Control-Allow-Origin不能为 *:当使用credentials: 'include'withCredentials: true时,服务器必须指定具体的源,不能使用通配符*
  2. Access-Control-Allow-Credentials必须为true:服务器必须明确允许携带凭证
  3. Cookie要求 :Cookie必须设置SameSite=NoneSecure属性(如果使用HTTPS)
  4. 预检请求:携带自定义头部的请求会触发预检请求(OPTIONS)
  5. 安全考虑:确保只允许信任的源访问,避免CSRF攻击
常见问题排查
  1. 错误:Credentials flag is 'true', but the 'Access-Control-Allow-Credentials' header is ''

    • 解决:确保服务器设置了Access-Control-Allow-Credentials: true
  2. 错误:The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*'

    • 解决:将Access-Control-Allow-Origin*改为具体的源地址
  3. Cookie未发送

    • 检查:前端是否设置了credentials: 'include'withCredentials: true
    • 检查:Cookie是否设置了SameSite=None; Secure
  4. 预检请求失败

    • 确保OPTIONS请求也返回正确的CORS头
    • 检查Access-Control-Allow-Headers是否包含所有需要的头部
完整示例:前后端配合

前端(React示例)

javascript 复制代码
import React, { useEffect } from 'react';
import axios from 'axios';

function App() {
    useEffect(() => {
        // 获取用户信息(需要携带Cookie)
        axios.get('https://api.example.com/user/profile', {
            withCredentials: true
        })
        .then(response => {
            console.log('用户信息:', response.data);
        })
        .catch(error => {
            console.error('获取用户信息失败:', error);
        });
    }, []);

    return <div>用户信息页面</div>;
}

export default App;

后端(Node.js + Express)

javascript 复制代码
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();

app.use(cookieParser());

// CORS配置
app.use((req, res, next) => {
    const allowedOrigins = ['https://www.example.com', 'https://app.example.com'];
    const origin = req.headers.origin;
    
    if (allowedOrigins.includes(origin)) {
        res.setHeader('Access-Control-Allow-Origin', origin);
    }
    
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    res.setHeader('Access-Control-Max-Age', '86400');
    
    if (req.method === 'OPTIONS') {
        return res.sendStatus(200);
    }
    next();
});

// 登录接口
app.post('/api/login', (req, res) => {
    // 验证用户凭据
    const { username, password } = req.body;
    
    if (username === 'admin' && password === 'password') {
        // 设置Cookie
        res.cookie('sessionId', 'abc123', {
            httpOnly: true,
            secure: true, // 仅HTTPS
            sameSite: 'none', // 允许跨站
            maxAge: 24 * 60 * 60 * 1000 // 24小时
        });
        
        res.json({ success: true, message: '登录成功' });
    } else {
        res.status(401).json({ success: false, message: '用户名或密码错误' });
    }
});

// 获取用户信息(需要验证Cookie)
app.get('/api/user/profile', (req, res) => {
    const sessionId = req.cookies.sessionId;
    
    if (sessionId === 'abc123') {
        res.json({
            username: 'admin',
            email: 'admin@example.com',
            role: 'administrator'
        });
    } else {
        res.status(401).json({ error: '未授权访问' });
    }
});

app.listen(3000, () => {
    console.log('服务器运行在 http://localhost:3000');
});

通过以上配置,即可实现安全的跨域Cookie传递。```

快速选型指南

按项目场景选择:
  1. 前后端分离项目 → CORS + Nginx反向代理
  2. 微前端/多应用集成 → postMessage + 反向代理
  3. 实时应用(聊天、推送) → WebSocket + CORS
  4. 调用第三方API → 服务器端转发
  5. 老旧系统兼容 → JSONP(仅限安全数据)或服务器端转发
  6. 子域系统集成 → document.domain
按技术栈选择:
  • 现代前端框架(React/Vue/Angular):CORS为主,开发环境用Webpack/Vite代理
  • 传统多页应用:反向代理或服务器端转发
  • Node.js后端:直接配置CORS中间件
  • Java/Spring Boot :使用@CrossOrigin注解或CORS配置类
  • Nginx部署:反向代理统一管理
按安全要求选择:
  • 高安全要求:CORS(指定具体源)+ HTTPS + 反向代理
  • 中等安全要求:CORS(指定域名)+ 反向代理
  • 兼容性优先:JSONP(仅限公开数据)+ 服务器端转发
按浏览器兼容性选择:
  • 现代浏览器(Chrome 80+/Firefox/Safari):优先CORS
  • 需要支持IE 10/11:CORS + 反向代理(IE部分支持CORS)
  • 需要支持IE 9及以下:JSONP或服务器端转发

核心决策原则

  1. 可控性优先:能控制目标服务器时,首选CORS
  2. 安全性优先:涉及敏感数据时,避免JSONP,使用CORS或服务器转发
  3. 实时性优先:需要双向通信时,选择WebSocket
  4. 兼容性优先:需要支持老旧浏览器时,考虑JSONP或服务器转发
  5. 维护性优先:长期项目优先选择标准方案(CORS/反向代理)

记住:没有一种方案适合所有场景,实际项目中常组合使用多种方案。开发环境多用代理,生产环境根据实际情况选择CORS或反向代理,特殊场景再考虑其他方案。

总结

跨域问题是现代Web开发中不可避免的技术挑战,源于浏览器的同源策略这一重要安全机制。本文系统性地介绍了8种主流跨域解决方案,每种方案都有其独特的适用场景和优缺点:

  1. CORS(跨域资源共享) - W3C标准方案,现代Web开发的首选,适合前后端分离架构
  2. 代理服务器 - 开发环境常用,生产环境可通过Nginx等反向代理实现
  3. JSONP - 兼容性极佳但安全性较差,仅适用于简单GET请求
  4. postMessage - 适用于窗口/iframe间通信,安全可控
  5. WebSocket - 实时双向通信的理想选择
  6. document.domain + iframe - 适用于同主域不同子域的场景
  7. window.name + iframe - 兼容老旧浏览器的备选方案
  8. 服务器端转发 - 调用第三方API的通用解决方案

核心选型建议

  • 现代Web应用:优先使用CORS,配合Nginx反向代理
  • 开发环境:使用Webpack/Vite的代理配置
  • 实时应用:考虑WebSocket
  • 老旧系统兼容:JSONP或服务器端转发
  • 第三方API调用:服务器端转发
  • 微前端/iframe通信:postMessage

关键原则:没有一种方案适合所有场景,实际项目中应根据技术栈、安全要求、浏览器兼容性和维护成本等因素综合考虑,常常需要组合使用多种方案。

参考资料

官方文档与标准

  1. W3C CORS规范 - Cross-Origin Resource Sharing
  2. MDN Web Docs - 同源策略
  3. MDN Web Docs - 跨源资源共享(CORS)
  4. WHATWG Fetch标准 - Fetch Living Standard
  5. HTML5规范 - postMessage API

技术文档与教程

  1. Express.js CORS中间件 - cors npm包文档
  2. Spring Framework - CORS支持
  3. Nginx反向代理配置 - Nginx文档
  4. WebSocket协议 - RFC 6455
  5. JSONP原理与安全 - JSONP安全指南

浏览器兼容性

  1. Can I use - CORS支持情况
  2. Can I use - WebSocket支持情况
  3. Can I use - postMessage支持情况

安全最佳实践

  1. OWASP安全指南 - 跨域资源共享安全
  2. Mozilla安全指南 - CORS安全配置
  3. Google Web安全 - 同源策略详解

工具与库

  1. CORS代理服务 - cors-anywhere
  2. 开发代理工具 - Webpack DevServer代理
  3. Vite开发服务器 - Vite服务器配置
  4. Postman测试工具 - 跨域请求测试

推荐阅读

  1. 《Web安全权威指南》 - 跨域安全章节
  2. 《HTTP权威指南》 - CORS协议详解
  3. MDN教程 - 使用CORS
  4. Google Developers - Same-Origin Policy

社区资源

  1. Stack Overflow - CORS相关问答
  2. GitHub讨论 - 各框架的CORS实现与问题
  3. CSDN博客 - 跨域解决方案实践案例

提示:随着Web标准的演进和浏览器的发展,跨域解决方案也在不断更新。建议定期查阅最新文档,关注W3C和浏览器厂商的更新,以确保使用最安全、最高效的跨域方案。

相关推荐
谙忆10244 分钟前
WebCodecs 实战:用 ImageDecoder 和 VideoFrame 在浏览器里做硬件加速的帧处理
前端
JerrySir7 分钟前
密码没泄露,2FA 也没失效:Chrome 146 为什么还要让 Cookie“搬不走”?
前端
小蚂蚁i7 分钟前
React Hooks 原理深度解析:从会用到真正懂
前端·react.js
触底反弹13 分钟前
Vue 实战:手把手教你实现 ChatGPT 同款打字机效果
前端·javascript·人工智能
BreezeJiang15 分钟前
Vue 3 流式输出实战:ReadableStream + DeepSeek API 实现打字机对话
前端
用户EasyAdminBlazor21 分钟前
EasyAdminBlazor 第十篇:数据导入导出——批量处理不再麻烦
前端·后端
饮茶三千23 分钟前
电子保函模板编辑器实战二:HTML→FTL 编译转换的设计与实现
前端·vue.js·设计模式
ricardo197328 分钟前
SSR / SSG 性能对比:Next.js 三种渲染模式实测数据
前端·面试
ikoala44 分钟前
Codex 小白入门:从安装到插件、MCP、Skills,一篇把配置讲明白
前端·javascript·后端
雪隐1 小时前
用Flutter做背单词APP-04一个人,2周,从零撸出一个背单词App
前端·人工智能·后端