前端解决跨域问题的方法

0 阅读11分钟

前端解决跨域问题的方法

概述

跨域问题是前端开发中常见的问题,由于浏览器的同源策略(Same-Origin Policy),当一个页面去访问另一个域名、协议或端口下的资源时,就会受到限制。本文将详细介绍前端解决跨域问题的各种方法及其弊端。


1. CORS(跨域资源共享)

原理

通过在服务器端设置响应头来允许跨域访问。这是目前最推荐、最标准的解决方案。

实现方式

服务器端配置
Access-Control-Allow-Origin: *  // 或指定具体域名
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Credentials: true
Express.js 示例
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'https://yourdomain.com');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  next();
});
前端请求示例
fetch('https://api.example.com/data', {
  method: 'GET',
  credentials: 'include',  // 携带凭证
  headers: {
    'Content-Type': 'application/json'
  }
})
.then(response => response.json())
.then(data => console.log(data));

弊端

  • 需要后端配合修改配置:前后端需要协调配置
  • 老旧浏览器不支持:IE10 以下不支持
  • 携带凭证时限制严格:设置 withCredentials: true 时不能使用 *,必须指定具体域名
  • 安全隐患:配置不当可能导致滥用
  • 预检请求开销:复杂请求会先发送 OPTIONS 预检请求,增加网络开销
  • 配置复杂:需要理解各种请求头的作用和配置方式

2. JSONP(JSON with Padding)

原理

利用 <script> 标签不受同源策略限制的特点,通过动态创建 script 标签来实现跨域请求。

实现方式

前端实现
// 定义回调函数
function handleResponse(data) {
  console.log('Received data:', data);
}

// 创建 script 标签
const script = document.createElement('script');
script.src = 'https://api.example.com/data?callback=handleResponse&param=value';
document.body.appendChild(script);

// 请求完成后移除 script 标签
script.onload = () => {
  document.body.removeChild(script);
};
服务器端返回格式
// Node.js 示例
app.get('/data', (req, res) => {
  const callback = req.query.callback;
  const data = { name: 'John', age: 30 };
  res.send(`${callback}(${JSON.stringify(data)})`);
});
封装的 JSONP 函数
function jsonp(url, callbackName) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    const callback = `jsonp_${Date.now()}`;

    window[callback] = (data) => {
      resolve(data);
      document.body.removeChild(script);
      delete window[callback];
    };

    script.src = `${url}${url.includes('?') ? '&' : '?'}callback=${callback}`;
    script.onerror = reject;
    document.body.appendChild(script);
  });
}

// 使用
jsonp('https://api.example.com/data')
  .then(data => console.log(data))
  .catch(error => console.error(error));

弊端

  • 只支持 GET 请求:不支持 POST、PUT、DELETE 等方法
  • 安全性较低:容易受到 XSS 攻击
  • 错误处理困难:无法使用 try-catch,错误处理不友好
  • 需要服务器端配合:服务器需要返回特定格式的数据
  • 无法使用现代 Web API:不能与 Fetch、Axios 等现代 HTTP 客户端配合使用
  • 已过时:不再推荐使用,正逐渐被 CORS 取代
  • 无法设置请求头:无法自定义 HTTP 请求头
  • 状态码无法获取:无法判断请求的真实状态码

3. 代理服务器(开发环境)

原理

在开发环境中配置代理,将请求转发到目标服务器,绕过浏览器的同源策略限制。

实现方式

Vite 配置
// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
        configure: (proxy, options) => {
          proxy.on('error', (err, req, res) => {
            console.log('proxy error', err);
          });
        }
      }
    }
  }
}
Webpack 配置
// webpack.config.js
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        },
        secure: false  // 如果是 HTTPS 接口,需要配置这个参数
      },
      '/another-api': {
        target: 'https://another-api.example.com',
        changeOrigin: true
      }
    }
  }
}
Create React App 配置
// 在 package.json 中配置
{
  "name": "my-app",
  "version": "0.1.0",
  "proxy": "https://api.example.com"
}
Next.js 配置
// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: 'https://api.example.com/:path*',
      },
    ];
  },
}
前端请求
// 配置后,直接使用相对路径请求
fetch('/api/users')
  .then(response => response.json())
  .then(data => console.log(data));

弊端

  • 仅适用于开发环境:生产环境无法使用,需要其他方案
  • 增加一层代理:可能影响性能,增加延迟
  • 配置相对复杂:不同框架需要不同的配置方式
  • 无法在生产环境使用:上线后需要配置其他跨域方案
  • 调试和日志查看困难:代理层的错误和日志不够直观
  • 需要重启开发服务器:修改配置后需要重启服务才能生效

4. Nginx 反向代理

原理

通过 Nginx 作为反向代理服务器处理跨域请求,Nginx 接收前端请求后转发到后端服务器。

实现方式

基础配置
server {
  listen 80;
  server_name yourdomain.com;

  location /api {
    proxy_pass https://api.example.com;
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';

    if ($request_method = 'OPTIONS') {
      return 204;
    }
  }
}
完整配置示例
server {
  listen 80;
  server_name yourdomain.com;

  # 处理 OPTIONS 预检请求
  if ($request_method = 'OPTIONS') {
    add_header 'Access-Control-Allow-Origin' '*';
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
    add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
    add_header 'Access-Control-Max-Age' 1728000;
    add_header 'Content-Type' 'text/plain; charset=utf-8';
    add_header 'Content-Length' 0;
    return 204;
  }

  location /api {
    proxy_pass https://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;

    # CORS 头设置
    add_header 'Access-Control-Allow-Origin' '*' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
  }
}
多后端服务配置
upstream backend1 {
  server backend1.example.com:8080;
}

upstream backend2 {
  server backend2.example.com:8080;
}

server {
  listen 80;
  server_name yourdomain.com;

  location /api/v1 {
    proxy_pass http://backend1;
    add_header Access-Control-Allow-Origin *;
  }

  location /api/v2 {
    proxy_pass http://backend2;
    add_header Access-Control-Allow-Origin *;
  }
}

弊点

  • 需要额外的服务器:需要独立的服务器和域名
  • 运维成本增加:需要配置和维护 Nginx 服务器
  • 单点故障风险:Nginx 宕机会影响所有服务
  • 需要服务器运维知识:需要了解 Nginx 配置和 Linux 运维
  • 调试相对复杂:问题排查需要查看 Nginx 日志
  • 增加部署复杂度:需要额外的部署流程和配置
  • 可能的性能损耗:增加了一层代理转发

5. WebSocket

原理

WebSocket 协议不受同源策略限制,可以用于建立跨域的双向通信连接。

实现方式

基础使用
// 创建 WebSocket 连接
const ws = new WebSocket('wss://api.example.com/ws');

// 连接打开
ws.onopen = () => {
  console.log('WebSocket connected');
  ws.send(JSON.stringify({
    type: 'hello',
    message: 'Hello Server'
  }));
};

// 接收消息
ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Received:', data);
};

// 错误处理
ws.onerror = (error) => {
  console.error('WebSocket error:', error);
};

// 连接关闭
ws.onclose = () => {
  console.log('WebSocket closed');
};
封装的 WebSocket 类
class WebSocketClient {
  constructor(url) {
    this.url = url;
    this.ws = null;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 5;
    this.reconnectInterval = 3000;
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log('WebSocket connected');
      this.reconnectAttempts = 0;
      this.startHeartbeat();
    };

    this.ws.onmessage = (event) => {
      this.handleMessage(JSON.parse(event.data));
    };

    this.ws.onerror = (error) => {
      console.error('WebSocket error:', error);
    };

    this.ws.onclose = () => {
      console.log('WebSocket closed');
      this.stopHeartbeat();
      this.attemptReconnect();
    };
  }

  send(data) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data));
    } else {
      console.error('WebSocket is not connected');
    }
  }

  startHeartbeat() {
    this.heartbeatInterval = setInterval(() => {
      this.send({ type: 'ping' });
    }, 30000);
  }

  stopHeartbeat() {
    if (this.heartbeatInterval) {
      clearInterval(this.heartbeatInterval);
    }
  }

  attemptReconnect() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++;
      setTimeout(() => {
        console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
        this.connect();
      }, this.reconnectInterval);
    }
  }

  handleMessage(data) {
    switch (data.type) {
      case 'pong':
        console.log('Heartbeat received');
        break;
      default:
        console.log('Received message:', data);
    }
  }

  close() {
    this.stopHeartbeat();
    if (this.ws) {
      this.ws.close();
    }
  }
}

// 使用
const wsClient = new WebSocketClient('wss://api.example.com/ws');
wsClient.connect();
wsClient.send({ type: 'message', content: 'Hello' });

弊端

  • 协议不适用于所有场景:主要用于实时通信,不适合普通的 API 请求
  • 需要服务器端支持:服务器必须支持 WebSocket 协议
  • 连接管理复杂:需要处理重连、心跳、断线重连等
  • 无法简单集成到现有架构:不像 HTTP 那样容易与现有系统集成
  • 调试和监控困难:WebSocket 连接的状态监控和问题排查相对复杂
  • 防火墙和代理支持:某些防火墙和代理可能不支持或限制 WebSocket
  • 资源占用:长连接会占用服务器资源

6. postMessage + iframe

原理

利用 iframe 和 postMessage API 进行跨域通信,实现不同域名页面之间的数据交换。

实现方式

父页面(主域名)
<!-- parent.html -->
<iframe id="childFrame" src="https://child.example.com/child.html" frameborder="0"></iframe>

<script>
  const iframe = document.getElementById('childFrame');

  // 监听子页面的消息
  window.addEventListener('message', (event) => {
    // 验证消息来源
    if (event.origin !== 'https://child.example.com') {
      return;
    }

    console.log('Received from child:', event.data);

    // 根据消息类型处理
    if (event.data.type === 'request') {
      // 处理请求并发送响应
      iframe.contentWindow.postMessage({
        type: 'response',
        data: 'Here is your response'
      }, 'https://child.example.com');
    }
  });

  // 向子页面发送消息
  function sendToChild(data) {
    iframe.contentWindow.postMessage(data, 'https://child.example.com');
  }

  // 使用示例
  setTimeout(() => {
    sendToChild({
      type: 'init',
      data: 'Hello from parent'
    });
  }, 1000);
</script>
子页面(跨域页面)
<!-- child.html -->
<h1>Child Page</h1>
<button onclick="sendToParent()">Send to Parent</button>

<script>
  // 监听父页面的消息
  window.addEventListener('message', (event) => {
    // 验证消息来源
    if (event.origin !== 'https://parent.example.com') {
      return;
    }

    console.log('Received from parent:', event.data);

    // 处理父页面的请求
    if (event.data.type === 'init') {
      console.log('Parent initialized:', event.data.data);
    }
  });

  // 向父页面发送消息
  function sendToParent() {
    window.parent.postMessage({
      type: 'request',
      data: 'Hello from child'
    }, 'https://parent.example.com');
  }

  // 监听响应
  window.addEventListener('message', (event) => {
    if (event.data.type === 'response') {
      console.log('Parent response:', event.data.data);
    }
  });
</script>
封装的跨域通信类
class CrossDomainMessenger {
  constructor(targetOrigin, targetWindow = window) {
    this.targetOrigin = targetOrigin;
    this.targetWindow = targetWindow;
    this.messageHandlers = new Map();

    this.init();
  }

  init() {
    window.addEventListener('message', (event) => {
      if (event.origin !== this.targetOrigin) {
        return;
      }

      const { type, data } = event.data;
      if (this.messageHandlers.has(type)) {
        this.messageHandlers.get(type)(data, event);
      }
    });
  }

  on(type, handler) {
    this.messageHandlers.set(type, handler);
  }

  off(type) {
    this.messageHandlers.delete(type);
  }

  send(type, data) {
    this.targetWindow.postMessage({ type, data }, this.targetOrigin);
  }

  request(type, data) {
    return new Promise((resolve) => {
      const requestId = `${type}_${Date.now()}`;

      const timeout = setTimeout(() => {
        this.off(requestId);
        resolve(null);
      }, 5000);

      this.on(requestId, (responseData) => {
        clearTimeout(timeout);
        this.off(requestId);
        resolve(responseData);
      });

      this.send(type, { ...data, requestId });
    });
  }
}

// 在父页面中使用
const childMessenger = new CrossDomainMessenger('https://child.example.com');
childMessenger.on('childMessage', (data) => {
  console.log('Child says:', data);
});

// 在子页面中使用
const parentMessenger = new CrossDomainMessenger('https://parent.example.com', window.parent);
parentMessenger.send('childMessage', { text: 'Hello parent!' });

弊端

  • 实现复杂:需要父页面和子页面配合,代码复杂度高
  • 性能开销大:加载 iframe 会增加页面加载时间和内存占用
  • 安全性需要严格控制:必须验证消息来源,否则可能被攻击
  • 不适用于 API 调用:主要用于页面间通信,不适合 API 请求场景
  • 用户体验可能受影响:iframe 加载可能影响页面交互
  • 难以进行错误处理:消息传递的错误处理相对困难
  • 调试困难:跨域通信的问题排查和调试较为复杂
  • SEO 影响:iframe 内容可能对 SEO 产生负面影响

7. 边缘计算服务代理(如 Cloudflare Workers)

原理

使用边缘计算服务(如 Cloudflare Workers、Vercel Edge Functions)作为代理,在边缘节点处理跨域请求。

实现方式

Cloudflare Workers 示例
// worker.js
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  const url = new URL(request.url);

  // 配置目标 API
  const targetUrl = 'https://api.example.com' + url.pathname + url.search;

  // 复制请求头
  const newHeaders = new Headers();
  request.headers.forEach((value, key) => {
    newHeaders.set(key, value);
  });

  // 发起请求
  const response = await fetch(targetUrl, {
    method: request.method,
    headers: newHeaders,
    body: request.body
  });

  // 创建响应并添加 CORS 头
  const newResponse = new Response(response.body, response);
  newResponse.headers.set('Access-Control-Allow-Origin', '*');
  newResponse.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  newResponse.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');

  return newResponse;
}
Vercel Edge Functions 示例
// app/api/proxy/[...path]/route.js
export async function GET(request, { params }) {
  const path = params.path.join('/');
  const targetUrl = `https://api.example.com/${path}${request.nextUrl.search}`;

  const response = await fetch(targetUrl, {
    headers: {
      'Content-Type': 'application/json',
    },
  });

  const data = await response.json();

  return Response.json(data, {
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
    },
  });
}

export async function POST(request, { params }) {
  const path = params.path.join('/');
  const body = await request.json();
  const targetUrl = `https://api.example.com/${path}`;

  const response = await fetch(targetUrl, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  });

  const data = await response.json();

  return Response.json(data, {
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
    },
  });
}

弊端

  • 需要额外的服务:需要注册和配置边缘计算服务
  • 可能产生额外费用:超出免费额度后需要付费
  • 依赖第三方服务:受限于服务提供商的功能和限制
  • 配置和调试复杂:需要了解特定平台的配置方式
  • 部署流程复杂:需要额外的部署和CI/CD配置
  • 数据隐私考虑:数据经过第三方服务,可能涉及隐私问题
  • 延迟可能增加:虽然边缘节点靠近用户,但增加了一层代理

方法对比总结

适用场景对比表

方法开发环境生产环境推荐指数适用场景
CORS⭐⭐⭐⭐⭐现代Web应用,需要标准解决方案
开发环境代理⭐⭐⭐⭐⭐本地开发调试
Nginx 反向代理⭐⭐⭐⭐有服务器运维能力的团队
WebSocket⭐⭐⭐实时通信、即时消息
postMessage + iframe⭐⭐⭐跨域页面间通信
JSONP老旧系统维护(不推荐)
边缘计算代理⭐⭐⭐⭐需要全球加速的场景

功能特性对比表

方法支持的 HTTP 方法携带凭证自定义请求头复杂度性能影响
CORS全部
开发环境代理全部
Nginx 反向代理全部
WebSocket--
postMessage + iframe---
JSONP仅 GET
边缘计算代理全部

最佳实践建议

1. 开发环境

推荐方案:使用框架提供的开发环境代理

// Vite 示例
export default {
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true
      }
    }
  }
}

优势

  • ✅ 配置简单,开箱即用
  • ✅ 不需要后端配合
  • ✅ 支持所有 HTTP 方法
  • ✅ 便于调试

2. 生产环境

推荐方案:优先使用 CORS

// 服务器端配置
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'https://yourdomain.com');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.header('Access-Control-Allow-Credentials', 'true');
  next();
});

优势

  • ✅ W3C 标准,广泛支持
  • ✅ 安全性好,可精细控制
  • ✅ 兼容性优秀
  • ✅ 支持所有 HTTP 方法

3. 有服务器运维能力

推荐方案:考虑使用 Nginx 反向代理

location /api {
  proxy_pass https://api.example.com;
  add_header Access-Control-Allow-Origin *;
}

优势

  • ✅ 集中管理多个后端服务
  • ✅ 可以统一处理 CORS、缓存、负载均衡
  • ✅ 性能优化空间大

4. 特殊场景

  • 实时通信:使用 WebSocket
  • 跨域页面通信:使用 postMessage + iframe
  • 老旧系统:考虑 JSONP(仅作为过渡方案)

5. 应该避免的方案

  • JSONP:已过时,存在安全隐患,不推荐在新项目中使用
  • document.domain:已被废弃,不建议使用
  • window.name:不安全,不推荐使用

总结

跨域问题是前端开发中不可避免的挑战,但有多种成熟的解决方案:

  1. CORS 是首选:标准、安全、兼容性好,是生产环境最推荐的方案
  2. 开发环境用代理:本地开发时使用框架提供的代理配置,简单高效
  3. Nginx 作为补充:有服务器运维能力的团队可以考虑 Nginx 反向代理
  4. 特殊场景特殊处理:实时通信用 WebSocket,页面通信用 postMessage
  5. 避免过时方案:JSONP 等老旧方案不建议在新项目中使用

选择合适的跨域解决方案需要综合考虑:

  • 应用场景(开发/生产)
  • 团队能力(运维、后端配合程度)
  • 性能要求
  • 安全要求
  • 兼容性要求

通过合理选择和配置跨域解决方案,可以有效解决跨域问题,提升应用的用户体验。

仅为个人平时记录