前端跨域请求原理及实践

在前端开发中,"跨域"是一个绕不开的话题。当我们的页面尝试从一个域名请求另一个域名的资源时,浏览器往往会抛出类似Access to fetch at 'xxx' from origin 'xxx' has been blocked by CORS policy的错误。下面将深入探讨跨域请求的底层原理,并介绍多种解决跨域问题和解决方案。

一、跨域的本质:同源策略

要理解跨域,首先需要了解浏览器的同源策略(Same-Origin Policy)。这是浏览器最核心的安全功能之一,由Netscape在1995年引入,其目的是防止恶意网页窃取另一个网页的敏感数据。

1.1 什么是"同源"?

两个URL被视为"同源",必须同时满足以下三个条件:

举例说明:

当前页面URL 请求目标URL 是否同源 原因
http://example.com http://example.com/page 三要素完全相同
http://example.com https://example.com 协议不同(http vs https)
http://example.com http://api.example.com 域名不同(主域 vs 子域)
http://example.com:80 http://example.com:8080 端口不同(80 vs 8080)

1.2 同源策略的限制范围

同源策略主要限制以下几种交互:

  • DOM访问:禁止不同源页面之间的DOM操作(如iframe嵌套的跨域页面)
  • 数据读取:禁止读取不同源的Cookie、LocalStorage、SessionStorage
  • 网络请求:禁止通过XMLHttpRequest、Fetch API等方式发起跨域HTTP请求

注意:并非所有跨域请求都会被禁止。像<img><script><link>等标签的资源加载不受同源策略限制,这也是后续某些跨域解决方案的技术基础。

二、跨域请求的类型:简单请求与预检请求

当浏览器检测到跨域请求时,会根据请求的特征将其分为两类,并采取不同的处理策略:

2.1 简单请求(Simple Request)

同时满足以下条件的请求被视为简单请求:

  1. 请求方法为以下三种之一:GETHEADPOST
  2. 请求头仅包含浏览器默认字段或以下字段:AcceptAccept-LanguageContent-LanguageContent-Type(仅限特定值)
  3. Content-Type的值只能是:application/x-www-form-urlencodedmultipart/form-datatext/plain

简单请求的处理流程

  1. 浏览器直接发送请求,并在请求头中添加Origin字段(值为当前页面域名)
  2. 服务器响应时,若包含Access-Control-Allow-Origin且值包含请求的Origin,则浏览器允许前端读取响应;否则拦截响应,抛出跨域错误

2.2 预检请求(Preflight Request)

不满足简单请求条件的跨域请求会触发预检请求,例如:

  • 使用PUTDELETE等特殊请求方法
  • 请求头包含自定义字段(如AuthorizationX-Requested-With
  • Content-Typeapplication/json

预检请求的处理流程

  1. 浏览器先发送一个OPTIONS方法的预检请求,询问服务器是否允许实际请求
  2. 服务器响应预检请求时,通过Access-Control-*系列头字段声明允许的跨域规则
  3. 若服务器允许,浏览器才发送实际请求;否则直接拦截,不发送实际请求

三、跨域解决方案及实践

了解跨域的原理后,我们来介绍几种常用的跨域解决方案,每种方案都将提供完整的代码示例。

3.1 CORS(Cross-Origin Resource Sharing)

CORS是W3C标准推荐的跨域解决方案,通过服务器端设置响应头实现跨域允许,支持所有HTTP方法,是目前最主流的跨域方案。

3.1.1 基本原理

CORS的核心是服务器端通过设置Access-Control-*系列响应头,告知浏览器允许哪些跨域请求。常用头字段包括:

  • Access-Control-Allow-Origin:允许的源(如https://example.com*表示允许所有)
  • Access-Control-Allow-Methods:允许的请求方法(如GET, POST, PUT
  • Access-Control-Allow-Headers:允许的请求头
  • Access-Control-Allow-Credentials:是否允许携带凭证(Cookie等)
  • Access-Control-Max-Age:预检请求的缓存时间(避免重复发送预检请求)
3.1.2 代码示例

前端代码(使用Fetch API)

javascript 复制代码
// 前端页面地址:http://localhost:3000
fetch('http://localhost:4000/api/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Custom-Header': 'custom-value' // 自定义头,会触发预检请求
  },
  body: JSON.stringify({ name: '前端请求' }),
  credentials: 'include' // 允许携带Cookie
})
.then(response => response.json())
.then(data => console.log('跨域请求成功:', data))
.catch(error => console.error('跨域请求失败:', error));

后端代码(Node.js + Express)

javascript 复制代码
// 服务器地址:http://localhost:4000
const express = require('express');
const app = express();
app.use(express.json());

// CORS配置中间件
app.use((req, res, next) => {
  // 允许的源(生产环境建议指定具体域名,而非*)
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000');
  
  // 允许的请求方法
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  
  // 允许的请求头(需包含前端实际使用的所有自定义头)
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Custom-Header');
  
  // 允许携带凭证(Cookie等)
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  
  // 预检请求缓存时间(秒)
  res.setHeader('Access-Control-Max-Age', '86400'); // 24小时
  
  // 处理预检请求(直接返回204)
  if (req.method === 'OPTIONS') {
    return res.sendStatus(204);
  }
  
  next();
});

// 接口路由
app.post('/api/data', (req, res) => {
  console.log('收到跨域请求数据:', req.body);
  res.json({ 
    status: 'success', 
    message: '跨域请求处理完成',
    data: req.body
  });
});

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

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

3.2 JSONP(JSON with Padding)

JSONP是一种古老但兼容性极佳的跨域方案(支持IE等老浏览器),其原理是利用<script>标签不受同源策略限制的特性,通过动态创建<script>标签发起跨域请求。

3.2.1 基本原理
  1. 前端定义一个回调函数(如handleJsonpResponse
  2. 前端动态创建<script>标签,其src指向跨域接口,并在URL中携带回调函数名(如?callback=handleJsonpResponse
  3. 服务器接收到请求后,将数据包裹在回调函数中返回(如handleJsonpResponse({...})
  4. 浏览器加载<script>后,自动执行回调函数,前端即可获取数据
3.2.2 代码示例

前端代码

html 复制代码
<!-- 前端页面地址:http://localhost:3000 -->
<script>
// 定义回调函数
function handleJsonpResponse(data) {
  console.log('JSONP跨域请求成功:', data);
}

// 动态创建script标签发起请求
function requestJsonp() {
  const script = document.createElement('script');
  // 跨域接口地址,携带回调函数名
  script.src = 'http://localhost:4000/api/jsonp?callback=handleJsonpResponse&name=jsonp请求';
  document.body.appendChild(script);
  
  // 请求完成后移除script标签
  script.onload = () => script.remove();
  script.onerror = () => {
    console.error('JSONP请求失败');
    script.remove();
  };
}
</script>

<button onclick="requestJsonp()">发起JSONP请求</button>

后端代码(Node.js + Express)

javascript 复制代码
// 服务器地址:http://localhost:4000
const express = require('express');
const app = express();

app.get('/api/jsonp', (req, res) => {
  const { callback, name } = req.query;
  console.log('收到JSONP请求参数:', name);
  
  // 构造响应数据(用回调函数包裹)
  const data = {
    status: 'success',
    message: 'JSONP请求处理完成',
    data: { name }
  };
  
  // 返回JavaScript代码(执行回调函数)
  res.send(`${callback}(${JSON.stringify(data)})`);
});

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

局限性

  • 仅支持GET请求
  • 安全性风险(可能遭受XSS攻击)
  • 无法捕获HTTP错误状态码(如404、500)

3.3 代理服务器

代理服务器是开发环境中常用的跨域解决方案,其原理是:由于浏览器的同源策略只限制前端脚本,不限制服务器之间的通信,因此可以通过一个与前端同源的代理服务器转发请求到目标服务器。

3.3.1 开发环境代理(以Vite为例)

在前端项目中(如Vue、React),可通过开发服务器配置代理,解决开发阶段的跨域问题。

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

javascript 复制代码
// 前端开发服务器:http://localhost:5173
export default {
  server: {
    // 配置代理
    proxy: {
      // 匹配所有以/api开头的请求
      '/api': {
        target: 'http://localhost:4000', // 目标服务器地址
        changeOrigin: true, // 发送请求时,将Host头改为目标服务器地址
        // 可选:重写路径(如果目标接口没有/api前缀)
        // rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
};

前端请求代码

javascript 复制代码
// 此时请求的是同源的开发服务器(http://localhost:5173),无跨域问题
// 开发服务器会自动转发到 http://localhost:4000/api/data
fetch('/api/data', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: '通过代理请求' })
})
.then(response => response.json())
.then(data => console.log('代理请求成功:', data));
3.3.2 生产环境代理(Nginx)

生产环境中,可通过Nginx反向代理实现跨域,配置示例如下:

nginx 复制代码
# Nginx配置
server {
  listen 80;
  server_name localhost;

  # 前端页面所在目录
  location / {
    root /path/to/frontend;
    index index.html;
  }

  # 代理跨域请求
  location /api/ {
    # 目标服务器地址
    proxy_pass http://localhost:4000/api/;
    # 传递原始请求头
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    # 可选:设置CORS头(如果目标服务器未设置)
    add_header Access-Control-Allow-Origin *;
  }
}

配置后,前端直接请求/api/data,Nginx会自动转发到http://localhost:4000/api/data,避免跨域问题。

3.4 其他跨域方案

3.4.1 iframe + postMessage

适用于两个跨域页面之间的通信(如父页面与iframe嵌套页面):

父页面(http://parent.com

html 复制代码
<iframe id="childFrame" src="http://child.com"></iframe>

<script>
// 向子页面发送消息
const frame = document.getElementById('childFrame');
frame.onload = () => {
  frame.contentWindow.postMessage(
    { type: 'greeting', data: 'Hello from parent' },
    'http://child.com' // 限制接收域
  );
};

// 接收子页面消息
window.addEventListener('message', (event) => {
  // 验证消息来源
  if (event.origin !== 'http://child.com') return;
  console.log('收到子页面消息:', event.data);
});
</script>

子页面(http://child.com

javascript 复制代码
// 接收父页面消息
window.addEventListener('message', (event) => {
  if (event.origin !== 'http://parent.com') return;
  console.log('收到父页面消息:', event.data);
  
  // 向父页面回复消息
  event.source.postMessage(
    { type: 'response', data: 'Hello from child' },
    event.origin
  );
});
3.4.2 WebSocket

WebSocket协议本身不受同源策略限制,可直接建立跨域连接:

前端代码

javascript 复制代码
// 建立WebSocket连接(ws/wss协议)
const socket = new WebSocket('ws://localhost:4000');

// 连接成功
socket.onopen = () => {
  console.log('WebSocket连接已建立');
  socket.send('Hello WebSocket');
};

// 接收消息
socket.onmessage = (event) => {
  console.log('收到WebSocket消息:', event.data);
};

后端代码(Node.js + ws库)

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

wss.on('connection', (ws) => {
  console.log('客户端已连接');
  ws.on('message', (message) => {
    console.log('收到消息:', message.toString());
    ws.send('服务器收到:' + message.toString());
  });
});

四、总结与最佳实践

跨域请求的解决方案各有优缺点,选择时需根据实际场景判断:

方案 优点 缺点 适用场景
CORS 功能完善、支持所有HTTP方法、安全性高 需要服务器配合、老浏览器兼容问题 现代Web应用(推荐)
JSONP 兼容性好(支持IE) 仅支持GET、安全性差 需兼容老浏览器的场景
代理服务器 前端无需修改、开发/生产均可用 需要额外配置服务器 开发环境调试、生产环境跨域
iframe + postMessage 适合页面间通信 仅用于页面交互、不适合API请求 父页面与iframe跨域通信
WebSocket 全双工通信、无跨域限制 需专门协议、不适合普通API请求 实时通信场景(如聊天、通知)

最佳实践建议

  1. 优先使用CORS,这是最标准、最安全的跨域方案
  2. 开发环境使用代理服务器(如Vite、Webpack代理)提高开发效率
  3. 生产环境避免使用*作为Access-Control-Allow-Origin,严格限制允许的源
  4. 涉及用户凭证的请求,确保正确配置Access-Control-Allow-Credentials
  5. 避免使用JSONP,除非有强烈的老浏览器兼容需求

通过本文的介绍,相信你已经对跨域请求的原理和解决方案有了全面的理解。在实际开发中,结合具体场景选择合适的方案,就能轻松解决跨域问题。

相关推荐
@大迁世界7 分钟前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路16 分钟前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug19 分钟前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu1213821 分钟前
React面向组件编程
开发语言·前端·javascript
持续升级打怪中43 分钟前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路1 小时前
GDAL 实现矢量合并
前端
hxjhnct1 小时前
React useContext的缺陷
前端·react.js·前端框架
冰暮流星1 小时前
javascript逻辑运算符
开发语言·javascript·ecmascript
前端 贾公子1 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端
菩提小狗1 小时前
Sqlmap双击运行脚本,双击直接打开。
前端·笔记·安全·web安全