什么是跨域问题

跨域问题(Cross-Origin Resource Sharing,简称CORS)是浏览器出于安全考虑实施的一种同源策略限制。当网页尝试从一个与当前页面不同源(协议、域名或端口不同)的服务器请求资源时,浏览器会阻止这种请求,除非目标服务器明确允许。

同源策略的定义

同源策略要求以下三个必须相同:

  • 协议相同(http/https)

  • 域名相同

  • 端口相同

例如:

  • http://example.com/index.html 和 http://example.com/api 是同源

  • http://example.com 和 https://example.com 不同源(协议不同)

  • http://example.com 和 http://api.example.com 不同源(域名不同)

  • http://example.com 和 http://example.com:8080 不同源(端口不同)

常见的跨域场景

  1. 前端应用和后端API部署在不同域名下

  2. 使用第三方API服务

  3. 开发环境中前端运行在本地服务器(如localhost:3000),而后端运行在其他端口或域名

  4. 使用CDN托管静态资源

跨域解决方案

1. CORS(跨域资源共享)

CORS是W3C标准,也是最主流的跨域解决方案。它通过在HTTP头中添加特定字段来实现跨域访问控制。

服务器端设置示例:

Access-Control-Allow-Origin: *  // 允许所有域名
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS  // 允许的请求方法
Access-Control-Allow-Headers: Content-Type, Authorization  // 允许的请求头
Access-Control-Allow-Credentials: true  // 是否允许发送cookie

Node.js Express框架实现示例:

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

// 允许所有来源的简单CORS设置
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  next();
});

// 更精细的控制
const allowedOrigins = ['http://example.com', 'http://localhost:3000'];
app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (allowedOrigins.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin);
    res.header('Access-Control-Allow-Credentials', 'true');
  }
  next();
});

2. JSONP(JSON with Padding)

JSONP是利用<script>标签不受同源策略限制的特性实现的跨域方案。

前端实现:

function handleResponse(data) {
  console.log('Received data:', data);
}

const script = document.createElement('script');
script.src = 'http://example.com/api?callback=handleResponse';
document.body.appendChild(script);

服务器端实现:

app.get('/api', (req, res) => {
  const callbackName = req.query.callback;
  const data = { message: 'Hello from JSONP!' };
  res.send(`${callbackName}(${JSON.stringify(data)})`);
});

限制:

  • 仅支持GET请求

  • 安全性较低

  • 无法处理HTTP错误状态

3. 代理服务器

通过同源的代理服务器转发请求,绕过浏览器的同源限制。

开发环境代理示例(webpack devServer):

// vue.config.js 或 webpack.config.js
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://backend.example.com',
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      }
    }
  }
}

Nginx反向代理配置:

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

  location /api/ {
    proxy_pass http://backend.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;
  }
}

4. WebSocket

WebSocket协议不受同源策略限制,可以实现跨域通信。

示例:

const socket = new WebSocket('ws://example.com/socket');

socket.onopen = function() {
  console.log('Connection established');
  socket.send('Hello Server!');
};

socket.onmessage = function(event) {
  console.log('Message from server:', event.data);
};

5. postMessage

window.postMessage API允许不同源窗口之间安全地进行通信。

示例:

// 发送方
const targetWindow = window.open('http://other-origin.com');
targetWindow.postMessage('Hello from origin A!', 'http://other-origin.com');

// 接收方
window.addEventListener('message', (event) => {
  if (event.origin !== 'http://origin-a.com') return;
  console.log('Received message:', event.data);
});

6. document.domain

对于具有相同二级域名的页面(如a.example.com和b.example.com),可以通过设置document.domain来实现跨域。

示例:

// 在两个页面中都设置
document.domain = 'example.com';

限制:

  • 仅适用于具有相同基础域名的页面

  • 必须将端口设置为null

  • 现代浏览器中限制较多

7. 浏览器扩展程序

Chrome扩展程序可以通过manifest.json配置跨域权限。

manifest.json示例:

{
  "name": "My Extension",
  "version": "1.0",
  "permissions": [
    "http://*.example.com/",
    "https://*.example.com/"
  ]
}

跨域请求中的特殊处理

预检请求(Preflight Request)

对于非简单请求(如带有自定义头或Content-Type不是application/x-www-form-urlencoded, multipart/form-data或text/plain的POST请求),浏览器会先发送OPTIONS预检请求。

服务器需要处理OPTIONS请求:

app.options('/api', (req, res) => {
  res.header('Access-Control-Allow-Origin', 'http://frontend.example.com');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.sendStatus(200);
});

携带凭证的请求

当请求需要携带cookie或HTTP认证信息时,需要特殊处理:

前端:

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

服务器:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://frontend.example.com  // 不能是*

实际开发中的最佳实践

  1. 开发环境:使用代理服务器(如webpack-dev-server的proxy配置)

  2. 生产环境:

    • 前后端同源部署

    • 或配置精确的CORS策略(避免使用*)

    • 或使用反向代理(Nginx/Apache)

  3. 第三方API:如果支持JSONP且仅需GET请求,可使用JSONP;否则需要后端代理

  4. 安全性:

    • 严格限制Access-Control-Allow-Origin

    • 对于敏感操作,避免在前端直接调用跨域API

    • 使用CSRF令牌保护表单提交

常见问题及解决方案

问题1:为什么设置了CORS头还是报跨域错误?

  • 检查Access-Control-Allow-Origin值是否精确匹配(包括协议、域名和端口)

  • 检查是否有多个Origin头

  • 对于带凭证的请求,确保不使用通配符*

问题2:OPTIONS请求返回404

  • 确保服务器正确处理了OPTIONS方法

  • 在路由处理前添加OPTIONS处理中间件

问题3:跨域请求无法携带cookie

  • 前端设置credentials: 'include'

  • 后端设置Access-Control-Allow-Credentials: true

  • 确保Access-Control-Allow-Origin不是通配符*

总结

跨域问题是前端开发中的常见挑战,但有多种解决方案可供选择。CORS是现代Web应用中最推荐的方式,它安全、灵活且被广泛支持。在开发环境中,代理服务器是最方便的解决方案。理解各种跨域技术的原理和适用场景,能够帮助开发者根据具体需求选择最合适的方案。

随着Web技术的发展,跨域解决方案也在不断演进。始终关注最新的安全实践和浏览器更新,确保应用的兼容性和安全性。

更多推荐