{T}

跨域解决方案

浏览器的同源策略是 Web 安全的基石,但在前后端分离架构下,跨域请求成为常见需求。本文详细介绍各种跨域解决方案的原理、配置与实践

概述

什么是同源策略

同源策略(Same-Origin Policy)是浏览器最核心的安全机制,它规定:协议(Protocol)、域名(Domain)、端口(Port)必须完全相同,才属于"同源"。

code
┌─────────────────────────────────────────────────────────────┐
│                    同源判断示例                              │
├─────────────────────────────────────────────────────────────┤
│  当前页面: https://www.example.com:443/page.html            │
├─────────────────────────────────────────────────────────────┤
│  URL                                      │ 同源 │ 原因      │
├───────────────────────────────────────────┼──────┼───────────┤
│  https://www.example.com/api/data        │  ✓   │ 完全相同  │
│  http://www.example.com/api/data         │  ✗   │ 协议不同  │
│  https://api.example.com/data            │  ✗   │ 域名不同  │
│  https://www.example.com:8080/api/data   │  ✗   │ 端口不同  │
│  https://example.com/api/data            │  ✗   │ 子域名不同│
└─────────────────────────────────────────────────────────────┘

跨域限制范围

同源策略主要限制以下行为:

受限行为说明示例
Ajax 请求无法读取跨域响应内容fetch('http://other.com/api')
Cookie无法读取跨域 Cookiedocument.cookie
DOM 操作无法操作跨域页面 DOMiframe.contentDocument
LocalStorage无法访问跨域存储localStorage.getItem()
IndexedDB无法访问跨域数据库indexedDB.open()

为什么需要跨域解决方案

在现代 Web 开发中,跨域需求非常普遍:

code
┌─────────────────────────────────────────────────────────────┐
│                     典型跨域场景                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────┐        ┌─────────────┐                    │
│  │ 前端应用     │        │ 后端 API     │                    │
│  │ localhost:  │ ────▶  │ api.example  │  开发环境跨域       │
│  │   3000      │        │   .com       │                    │
│  └─────────────┘        └─────────────┘                    │
│                                                             │
│  ┌─────────────┐        ┌─────────────┐                    │
│  │ 主站        │        │ 子系统       │                    │
│  │ www.a.com   │ ────▶  │ app.a.com    │  微前端架构         │
│  └─────────────┘        └─────────────┘                    │
│                                                             │
│  ┌─────────────┐        ┌─────────────┐                    │
│  │ 第三方服务   │        │ 开放平台      │                    │
│  │ client.com  │ ────▶  │ open.api.com │  公共 API 调用      │
│  └─────────────┘        └─────────────┘                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

一、CORS(跨域资源共享)

CORS(Cross-Origin Resource Sharing)是 W3C 标准,是目前最主流的跨域解决方案。它通过在服务器设置响应头来告知浏览器允许跨域访问。

基本原理

code
┌─────────────┐                              ┌─────────────┐
│   浏览器     │                              │   服务器     │
│             │                              │             │
│  发送请求    │  ① Request                  │             │
│  Origin:    │ ─────────────────────────────▶│             │
│  http://a.. │                              │  检查 Origin │
│             │                              │             │
│             │  ② Response                 │  设置响应头  │
│             │ ◀─────────────────────────────│             │
│             │  Access-Control-Allow-Origin │             │
│             │  Access-Control-Allow-Methods│             │
│             │                              │             │
│  允许跨域    │                              │             │
│  访问响应    │                              │             │
└─────────────┘                              └─────────────┘

简单请求

满足以下所有条件的请求为简单请求,不会触发预检:

请求方法:

  • GET
  • POST
  • HEAD

请求头部:

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type(仅限 application/x-www-form-urlencodedmultipart/form-datatext/plain
javascript
// 简单请求示例 - GET 请求
fetch('http://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data));

// 简单请求示例 - POST 表单数据
fetch('http://api.example.com/submit', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  body: 'name=Alice&age=25'
});

服务器端处理:

javascript
// Express 示例
app.get('/api/data', (req, res) => {
  // 允许所有来源
  res.header('Access-Control-Allow-Origin', '*');
  res.json({ data: 'success' });
});

// 更安全的配置 - 指定来源
app.get('/api/data', (req, res) => {
  const allowedOrigins = ['http://example.com', 'http://localhost:3000'];
  const origin = req.headers.origin;
  
  if (allowedOrigins.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin);
  }
  res.json({ data: 'success' });
});

预检请求

不满足简单请求条件时,浏览器会先发送 OPTIONS 请求进行预检:

code
┌─────────────┐                              ┌─────────────┐
│   浏览器     │                              │   服务器     │
│             │                              │             │
│             │  ① OPTIONS 预检请求          │             │
│             │ ─────────────────────────────▶│             │
│             │  Origin: http://example.com  │  检查权限    │
│             │  Access-Control-Request-     │             │
│             │  Method: PUT                 │             │
│             │                              │             │
│             │  ② 预检响应                  │             │
│             │ ◀─────────────────────────────│             │
│             │  Access-Control-Allow-Origin │             │
│             │  Access-Control-Allow-Methods│             │
│             │  Access-Control-Max-Age      │             │
│             │                              │             │
│             │  ③ 实际请求                  │             │
│             │ ─────────────────────────────▶│             │
│             │                              │             │
│             │  ④ 实际响应                  │             │
│             │ ◀─────────────────────────────│             │
└─────────────┘                              └─────────────┘
javascript
// 触发预检的请求示例
fetch('http://api.example.com/data', {
  method: 'PUT',  // 非简单方法
  headers: {
    'Content-Type': 'application/json',  // 非简单 Content-Type
    'Authorization': 'Bearer token'      // 自定义头部
  },
  body: JSON.stringify({ name: 'Alice' })
});

服务器端处理预检:

javascript
// Express 处理预检请求
app.options('/api/*', (req, res) => {
  res.header('Access-Control-Allow-Origin', 'http://example.com');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
  res.header('Access-Control-Max-Age', '86400');  // 预检结果缓存 24 小时
  res.sendStatus(204);
});

// 或者使用 cors 中间件
const cors = require('cors');

app.use(cors({
  origin: 'http://example.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  maxAge: 86400
}));

携带身份凭证

默认情况下,跨域请求不携带 Cookie。如需携带,需同时配置客户端和服务器:

javascript
// 客户端配置
// Fetch API
fetch('http://api.example.com/data', {
  credentials: 'include'  // 包含 Cookie
});

// XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.withCredentials = true;

// Axios
axios.get('http://api.example.com/data', {
  withCredentials: true
});
javascript
// 服务器端配置
app.get('/api/data', (req, res) => {
  // 不能使用 *,必须指定具体来源
  res.header('Access-Control-Allow-Origin', 'http://example.com');
  res.header('Access-Control-Allow-Credentials', 'true');
  res.json({ data: 'success' });
});

// 使用 cors 中间件
app.use(cors({
  origin: 'http://example.com',  // 不能为 '*'
  credentials: true
}));

响应头详解

响应头说明示例值
Access-Control-Allow-Origin允许的来源*http://example.com
Access-Control-Allow-Methods允许的方法GET, POST, PUT
Access-Control-Allow-Headers允许的请求头Content-Type, Authorization
Access-Control-Allow-Credentials是否允许携带凭证true
Access-Control-Max-Age预检结果缓存时间(秒)86400
Access-Control-Expose-Headers暴露给客户端的响应头X-Custom-Header
javascript
// Access-Control-Expose-Headers 示例
app.get('/api/data', (req, res) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Expose-Headers', 'X-Total-Count, X-Page-Size');
  res.header('X-Total-Count', '100');
  res.header('X-Page-Size', '10');
  res.json({ data: [] });
});

// 客户端获取自定义响应头
fetch('http://api.example.com/data')
  .then(response => {
    console.log(response.headers.get('X-Total-Count'));  // 100
    console.log(response.headers.get('X-Page-Size'));    // 10
  });

服务器配置示例

Express:

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

// 基础 CORS 配置
app.use(cors());

// 自定义 CORS 配置
const corsOptions = {
  origin: function (origin, callback) {
    const whitelist = ['http://example.com', 'http://localhost:3000'];
    if (!origin || whitelist.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400
};

app.use(cors(corsOptions));

Koa:

javascript
const Koa = require('koa');
const cors = require('@koa/cors');
const app = new Koa();

app.use(cors({
  origin: 'http://example.com',
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowHeaders: ['Content-Type', 'Authorization'],
  credentials: true
}));

Nginx:

nginx
server {
  location /api {
    # 允许的来源
    add_header 'Access-Control-Allow-Origin' '$http_origin';
    
    # 允许的方法
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
    
    # 允许的头部
    add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
    
    # 允许携带凭证
    add_header 'Access-Control-Allow-Credentials' 'true';
    
    # 预检缓存时间
    add_header 'Access-Control-Max-Age' 86400;
    
    # 处理预检请求
    if ($request_method = 'OPTIONS') {
      return 204;
    }
    
    proxy_pass http://backend;
  }
}

错误处理与调试

常见错误信息:

code
Access to fetch at 'http://api.example.com/data' from origin 
'http://localhost:3000' has been blocked by CORS policy: 
No 'Access-Control-Allow-Origin' header is present on the requested resource.

调试技巧:

javascript
// 1. 检查请求是否触发预检
fetch('http://api.example.com/data', {
  method: 'GET',
  headers: {
    'X-Custom-Header': 'value'  // 触发预检
  }
}).catch(error => console.error('CORS Error:', error));

// 2. 使用 Chrome DevTools 查看详情
// Network → 请求 → Headers → Response Headers

// 3. 服务器端调试
app.use((req, res, next) => {
  console.log('Request Origin:', req.headers.origin);
  console.log('Request Method:', req.method);
  console.log('Request Headers:', req.headers);
  next();
});

错误处理最佳实践:

javascript
// 客户端封装
async function fetchWithCORS(url, options = {}) {
  try {
    const response = await fetch(url, {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...options.headers
      }
    });
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    return await response.json();
  } catch (error) {
    if (error.message.includes('CORS')) {
      console.error('跨域错误,请检查服务器 CORS 配置');
    }
    throw error;
  }
}

二、JSONP

JSONP(JSON with Padding)是早期跨域解决方案,利用 <script> 标签不受同源策略限制的特性。

基本原理

code
┌─────────────┐                              ┌─────────────┐
│   浏览器     │                              │   服务器     │
│             │                              │             │
│  创建script │  ① <script src="url?        │             │
│  标签       │     callback=func">          │             │
│             │ ─────────────────────────────▶│             │
│             │                              │  执行回调    │
│             │  ② func({...data...})       │  包装数据    │
│             │ ◀─────────────────────────────│             │
│             │                              │             │
│  执行回调    │                              │             │
│  获取数据    │                              │             │
└─────────────┘                              └─────────────┘

实现方式

客户端实现:

javascript
/**
 * JSONP 请求封装
 * @param {string} url - 请求地址
 * @param {object} params - 请求参数
 * @param {string} callbackParam - 回调参数名,默认 callback
 * @returns {Promise} 返回 Promise
 */
function jsonp(url, params = {}, callbackParam = 'callback') {
  return new Promise((resolve, reject) => {
    // 生成唯一回调函数名
    const callbackName = `jsonp_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
    
    // 创建 script 元素
    const script = document.createElement('script');
    
    // 将回调函数挂载到全局,供服务器调用
    window[callbackName] = function(data) {
      resolve(data);
      // 请求完成后清理
      delete window[callbackName];
      document.body.removeChild(script);
    };
    
    // 构建 URL:合并 params 和 callback 参数
    const query = Object.entries({ ...params, [callbackParam]: callbackName })
      .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
      .join('&');
    const fullUrl = url + (url.includes('?') ? '&' : '?') + query;
    
    // 处理加载错误
    script.onerror = function() {
      reject(new Error('JSONP 请求失败'));
      delete window[callbackName];
      document.body.removeChild(script);
    };
    
    script.src = fullUrl;
    document.body.appendChild(script);
  });
}

// 使用示例
jsonp('http://api.example.com/data', { id: 123 })
  .then(data => console.log('成功:', data))
  .catch(error => console.error('失败:', error));

服务器端实现:

javascript
// Express
app.get('/api/data', (req, res) => {
  const callback = req.query.callback;
  const data = { name: 'Alice', age: 25 };
  
  if (callback) {
    // JSONP 响应
    res.type('text/javascript');
    res.send(`${callback}(${JSON.stringify(data)})`);
  } else {
    // 普通 JSON 响应
    res.json(data);
  }
});

优缺点分析

优点缺点
兼容性好,支持 IE6+只支持 GET 请求
实现简单,无需服务器额外配置存在安全风险(XSS)
不受同源策略限制无法获取响应状态码
需要服务器配合修改

安全注意事项

javascript
// 安全风险示例 - 恶意代码注入
// 服务器返回: callback({data: '</script><script>alert("XSS")</script>'});

// 安全实践 1: 验证回调函数名
app.get('/api/data', (req, res) => {
  const callback = req.query.callback;
  
  // 只允许合法的函数名
  if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(callback)) {
    return res.status(400).send('Invalid callback name');
  }
  
  const data = { name: 'Alice' };
  res.type('text/javascript');
  res.send(`${callback}(${JSON.stringify(data)})`);
});

// 安全实践 2: 限制来源
app.get('/api/data', (req, res) => {
  const referer = req.headers.referer || '';
  const allowedOrigins = ['http://example.com', 'http://localhost:3000'];
  
  if (!allowedOrigins.some(origin => referer.startsWith(origin))) {
    return res.status(403).send('Forbidden');
  }
  
  // ... 正常响应
});

三、代理服务器

代理服务器是常用的跨域解决方案,通过同源服务器转发请求,避免浏览器跨域限制。

工作原理

code
┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│   浏览器     │      │   代理服务器  │      │   目标服务器  │
│             │      │             │      │             │
│  发送请求    │ ───▶ │  接收请求    │ ───▶ │  处理请求    │
│  同源请求    │      │  转发请求    │      │  返回响应    │
│             │ ◀─── │  返回响应    │ ◀─── │             │
└─────────────┘      └─────────────┘      └─────────────┘
     同源               无跨域限制            无需配置 CORS

开发环境代理

Webpack Dev Server:

javascript
// webpack.config.js
module.exports = {
  devServer: {
    proxy: {
      // 基础配置
      '/api': {
        target: 'http://api.example.com',
        changeOrigin: true
      },
      
      // 完整配置
      '/api/v2': {
        target: 'http://api.example.com',
        changeOrigin: true,
        secure: false,           // 允许 https 目标
        pathRewrite: { '^/api/v2': '/v2' },
        headers: { 'X-Proxy': 'dev-server' },  // 自定义请求头
        onProxyReq: (proxyReq, req, res) => {
          console.log('代理请求:', req.url);
        }
      },
      '/service-b': {
        target: 'http://service-b.example.com',
        pathRewrite: { '^/service-b': '' }
      }
    }
  }
};

Vite:

javascript
// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://api.example.com',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/api/, ''),
        configure: (proxy, options) => {
          // 自定义代理配置
          proxy.on('proxyReq', (proxyReq, req) => {
            console.log('代理请求:', req.url);
          });
        }
      }
    }
  }
});

生产环境代理

Nginx 反向代理:

nginx
# nginx.conf
server {
  listen 80;
  server_name example.com;
  
  # 前端静态资源
  location / {
    root /var/www/html;
    try_files $uri $uri/ /index.html;
  }
  
  # API 代理
  location /api {
    proxy_pass http://api.example.com;
    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;
  }
  
  location /service-b {
    proxy_pass http://service-b.example.com;
  }
}

Node.js 中间件代理

http-proxy-middleware:

javascript
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();

// 基础代理
app.use('/api', createProxyMiddleware({
  target: 'http://api.example.com',
  changeOrigin: true,
  pathRewrite: { '^/api': '' }
}));

// 高级配置
app.use('/api', createProxyMiddleware({
  target: 'http://api.example.com',
  changeOrigin: true,
  ws: true,                    // 支持 WebSocket 代理
  secure: false,               // 忽略 https 证书校验
  pathRewrite: { '^/api': '' },
  onProxyReq: function(proxyReq, req, res) {
    console.log('代理请求:', req.method, req.url);
  },
  onProxyRes: function(proxyRes, req, res) {
    console.log('代理响应:', proxyRes.statusCode);
  },
  onError: function(err, req, res) {
    res.status(500).json({ error: 'Proxy error' });
  }
}));

app.listen(3000);

四、postMessage

window.postMessage 是 HTML5 提供的跨文档通信 API,用于不同窗口之间的安全通信。

跨窗口通信

javascript
/**
 * 父页面发送消息
 */
// 获取 iframe 引用
const iframe = document.getElementById('myIframe');

// 等待 iframe 加载完成
iframe.onload = function() {
  // 发送消息
  iframe.contentWindow.postMessage(
    { type: 'USER_DATA', data: { name: 'Alice' } },
    'http://child.example.com'  // 目标来源,必须指定
  );
};

// 父页面监听子页面回复
window.addEventListener('message', function(event) {
  // 必须校验来源
  if (event.origin !== 'http://child.example.com') return;
  console.log('收到子页面消息:', event.data);
});

子页面接收和回复:

javascript
// child.html 中
window.addEventListener('message', function(event) {
  // 校验来源(必须)
  if (event.origin !== 'http://parent.example.com') return;

  // 回复消息
  event.source.postMessage(
    { type: 'REPLY', data: '已收到' },
    event.origin
  );
});

安全验证机制

javascript
/**
 * 安全的 postMessage 封装
 */
class SecureMessenger {
  constructor(targetWindow, allowedOrigins) {
    this.targetWindow = targetWindow;
    this.allowedOrigins = allowedOrigins;
    this.listeners = new Map();
    
    window.addEventListener('message', this.handleMessage.bind(this));
  }
  
  // 发送消息
  send(type, data) {
    this.targetWindow.postMessage({ type, data }, '*');
  }
  
  // 监听消息
  on(type, callback) {
    if (!this.listeners.has(type)) {
      this.listeners.set(type, []);
    }
    this.listeners.get(type).push(callback);
  }
  
  // 处理收到的消息
  handleMessage(event) {
    // 校验来源(安全关键)
    if (!this.allowedOrigins.includes(event.origin)) {
      console.warn('拒绝来自未知来源的消息:', event.origin);
      return;
    }
    
    const { type, data } = event.data || {};
    const callbacks = this.listeners.get(type);
    if (callbacks) {
      callbacks.forEach(callback => callback(data, event));
    }
  }
  
  // 销毁时移除监听器
  destroy() {
    window.removeEventListener('message', this.handleMessage);
  }
}

// 使用示例
const messenger = new SecureMessenger(iframe.contentWindow, ['http://child.example.com']);
messenger.send('GET_DATA', { id: 123 });

// 接收消息
messenger.on('DATA_RESPONSE', (data, event) => {
  console.log('收到数据:', data);
});

实际应用场景

场景 1: iframe 高度自适应

javascript
// 子页面(iframe 内)
function sendHeight() {
  const height = document.body.scrollHeight;
  parent.postMessage({ type: 'RESIZE', height }, '*');
}

// 监听 DOM 变化
new ResizeObserver(sendHeight).observe(document.body);
window.addEventListener('load', sendHeight);

// 父页面
window.addEventListener('message', function(event) {
  if (event.data.type === 'RESIZE') {
    document.getElementById('myIframe').style.height = event.data.height + 'px';
  }
});

场景 2: 跨域登录状态同步

javascript
// 主站 a.com
window.addEventListener('message', function(event) {
  if (event.origin === 'https://sso.example.com') {
    if (event.data.type === 'LOGIN_SUCCESS') {
      localStorage.setItem('token', event.data.token);
      location.reload();
    }
  }
});

// SSO 登录页
window.opener.postMessage(
  { type: 'LOGIN_SUCCESS', token: 'xxx' },
  'https://a.com'
);
window.close();

五、WebSocket

WebSocket 协议不受同源策略限制,可用于跨域双向通信。

跨域特性

code
┌─────────────┐                              ┌─────────────┐
│   浏览器     │                              │   服务器     │
│             │                              │             │
│  WebSocket  │  ① WebSocket 握手            │             │
│  连接请求    │ ─────────────────────────────▶│             │
│             │  Origin: http://example.com  │             │
│             │                              │             │
│             │  ② 握手响应                  │             │
│             │ ◀─────────────────────────────│             │
│             │                              │             │
│             │  ③ 双向通信                  │             │
│             │ ◀────────────────────────────▶│             │
└─────────────┘                              └─────────────┘

连接管理

javascript
/**
 * WebSocket 客户端封装
 */
class WebSocketClient {
  constructor(url, options = {}) {
    this.url = url;
    this.options = options;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
    this.reconnectInterval = options.reconnectInterval || 3000;
    this.listeners = new Map();
    this.connect();
  }
  
  // 建立连接
  connect() {
    this.ws = new WebSocket(this.url);
    
    this.ws.onopen = () => {
      console.log('WebSocket 已连接');
      this.reconnectAttempts = 0;
      this.emit('open');
    };
    
    this.ws.onmessage = (event) => {
      let data;
      try {
        data = JSON.parse(event.data);
      } catch {
        data = event.data;
      }
      this.emit('message', data);
    };
    
    this.ws.onclose = () => {
      console.log('连接关闭');
      this.emit('close');
      // 自动重连
      if (this.reconnectAttempts < this.maxReconnectAttempts) {
        this.reconnectAttempts++;
        const delay = this.reconnectInterval * this.reconnectAttempts;
        console.log(`${delay}ms 后重连(第 ${this.reconnectAttempts} 次)`);
        setTimeout(() => this.connect(), delay);
      }
    };
    
    this.ws.onerror = (error) => {
      console.error('WebSocket 错误:', error);
      this.emit('error', error);
    };
  }
  
  // 发送消息
  send(data) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data));
    }
  }
  
  // 监听事件
  on(type, callback) {
    if (!this.listeners.has(type)) {
      this.listeners.set(type, []);
    }
    this.listeners.get(type).push(callback);
  }
  
  // 触发事件
  emit(type, ...args) {
    const callbacks = this.listeners.get(type);
    if (callbacks) {
      callbacks.forEach(cb => cb(...args));
    }
  }
  
  // 关闭连接
  close() {
    if (this.ws) this.ws.close();
  }
}

// 使用
const client = new WebSocketClient('ws://api.example.com/socket');

client.send({ type: 'SUBSCRIBE', channel: 'news' });
});

client.on('message', (data) => {
  console.log('收到消息:', data);
});

实践示例

javascript
// 服务器端(Node.js + ws)
const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });

server.on('connection', (ws, req) => {
  const origin = req.headers.origin;
  
  // 可选:验证 Origin
  const allowedOrigins = ['http://example.com', 'http://localhost:3000'];
  if (!allowedOrigins.includes(origin)) {
    ws.close();
    return;
  }
  
  // 接收客户端消息
  ws.on('message', (message) => {
    const data = JSON.parse(message);
    console.log('收到:', data);
    // 广播或回显
    ws.send(JSON.stringify({ type: 'ECHO', data }));
  });
  
  // 连接关闭
  ws.on('close', () => {
    console.log('客户端断开连接');
  });
};

// 客户端(浏览器)
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = function() {
  console.log('已连接');
  ws.send(JSON.stringify({ type: 'HELLO', msg: '你好' }));
};

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

六、其他方案

以下方案已过时,但在特定场景仍有参考价值。

document.domain

适用于同一主域名下的子域名通信(已废弃):

javascript
// a.example.com 页面
document.domain = 'example.com';

// b.example.com 页面
document.domain = 'example.com';

// 现在可以互相访问 DOM
// 注意:此方法已被现代浏览器废弃,不推荐使用

window.name

利用 window.name 跨页面传递数据:

javascript
/**
 * window.name 跨域方案
 */
function getDataFromUrl(url, callback) {
  let iframe = document.createElement('iframe');
  iframe.style.display = 'none';
  
  let state = 0;
  
  iframe.onload = function() {
    if (state === 0) {
      state = 1;
      // 第一次加载同源空页面
      iframe.src = 'about:blank';
    } else if (state === 1) {
      // 第二次加载完成后,读取 window.name
      state = 2;
      const data = iframe.contentWindow.name;
      callback(JSON.parse(data));
      document.body.removeChild(iframe);
    }
  };
  
  iframe.src = url;
  document.body.appendChild(iframe);
}

// 使用
getDataFromUrl('http://other-domain.com/data.html', (data) => {
  console.log('获取到数据:', data);
});

// data.html 内容
// <script>
//   window.name = '{"name":"Alice","age":25}';
// </script>

方案对比与选择

方案适用场景优点缺点
CORSAPI 请求、前后端分离项目标准化、支持所有 HTTP 方法、浏览器原生支持需要服务器配置
JSONP兼容老系统、简单 GET 请求兼容性好、实现简单只支持 GET、有安全风险
代理服务器开发环境、无法修改服务端的项目前端无需修改、对后端透明需要额外部署代理服务
postMessageiframe 嵌套、跨窗口通信安全可控、双向通信需要双方配合
WebSocket实时通信、推送服务双向通信、低延迟需要 WebSocket 服务端支持

选择建议:

code
┌─────────────────────────────────────────────────────────────┐
│                     方案选择决策树                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  需要跨域请求 API?                                          │
│       │                                                     │
│       ├─── 是 ──▶ 能否修改服务器?                           │
│       │              │                                      │
│       │              ├─── 是 ──▶ 使用 CORS                   │
│       │              │                                      │
│       │              └─── 否 ──▶ 使用代理服务器               │
│       │                                                     │
│       └─── 否 ──▶ iframe 通信?                             │
│                      │                                      │
│                      ├─── 是 ──▶ 使用 postMessage            │
│                      │                                      │
│                      └─── 否 ──▶ 实时通信?                   │
│                                       │                     │
│                                       ├─── 是 ──▶ WebSocket  │
│                                       │                     │
│                                       └─── 否 ──▶ JSONP     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

安全最佳实践

javascript
// 1. CORS 配置不要过于宽松
// ❌ 危险配置
app.use(cors({ origin: '*' }));  // 允许任何来源
app.use(cors({ origin: '*', credentials: true }));  // 无效配置

// ✅ 安全配置
const allowedOrigins = ['https://example.com', 'https://app.example.com'];
app.use(cors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true  // 允许携带 Cookie
}));

// 2. 服务端验证 WebSocket Origin(防止 CSRF 跨站攻击)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080, verifyClient: (info) => {
  const allowedOrigins = ['http://example.com', 'http://localhost:3000'];
  if (!allowedOrigins.includes(info.origin)) {
    return false;  // 拒绝连接
  }
  return true;
}});

wss.on('connection', (ws, req) => {
  if (!allowedOrigins.includes(req.headers.origin)) {
    ws.close(1008, 'Origin not allowed');
    return;
  }
  
  // 正常处理
});

常见问题解答

Q1: 为什么配置了 CORS 还是报跨域错误?

A: 常见原因:

  1. 服务器未正确设置响应头

    javascript
    // 检查是否在所有需要的路由上都设置了 CORS
    app.use(cors());  // 确保在路由之前
  2. 预检请求未处理

    javascript
    // 确保处理 OPTIONS 请求
    app.options('*', cors());
  3. 携带凭证时使用了 *

    javascript
    // ❌ 错误
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Credentials', 'true');
    
    // ✅ 正确
    res.header('Access-Control-Allow-Origin', 'http://example.com');
    res.header('Access-Control-Allow-Credentials', 'true');

Q2: 本地开发时如何处理跨域?

A: 推荐使用开发服务器代理:

javascript
// Vite
export default {
  server: {
    proxy: {
      '/api': 'http://api.example.com'
    }
  }
}

// Webpack
module.exports = {
  devServer: {
    proxy: {
      '/api': 'http://api.example.com'
    }
  }
}

Q3: 如何调试跨域问题?

A: 使用浏览器开发者工具:

  1. 打开 DevTools → Network
  2. 查看请求 Headers 中的 Origin
  3. 查看响应 Headers 中的 CORS 相关字段
  4. Console 中查看具体错误信息
javascript
// 在服务器端添加调试日志
app.use((req, res, next) => {
  console.log('Request Origin:', req.headers.origin);
  console.log('Request Method:', req.method);
  next();
});

Q4: CORS 会影响性能吗?

A: 对于非简单请求,会产生额外的预检请求开销。优化方法:

  1. 使用简单请求:尽量使用 GET、POST,避免复杂请求头
  2. 缓存预检结果:设置 Access-Control-Max-Age
  3. 合并请求:减少跨域请求次数
javascript
// 设置预检缓存
res.header('Access-Control-Max-Age', '86400');  // 24 小时

Q5: 如何实现多域名 CORS?

A: 动态设置 Access-Control-Allow-Origin

javascript
app.use((req, res, next) => {
  const allowedOrigins = [
    'https://example.com',
    'https://app.example.com',
    'http://localhost:3000'
  ];
  
  const origin = req.headers.origin;
  
  if (allowedOrigins.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin);
  }
  
  next();
});

💡 提示:CORS 是现代 Web 跨域解决方案的首选,掌握其原理和配置对前后端开发都至关重要。在生产环境中,务必注意安全配置,避免使用过于宽松的跨域策略。