前端跨域请求原理及实践

在前端开发中,"跨域"是一个绕不开的话题。当我们的页面尝试从一个域名请求另一个域名的资源时,浏览器往往会抛出类似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,除非有强烈的老浏览器兼容需求

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

相关推荐
不断努力的根号七25 分钟前
qt框架,使用webEngine如何调试前端
开发语言·前端·qt
德育处主任35 分钟前
p5.js 线段的用法
javascript·数据可视化·canvas
伍哥的传说2 小时前
React性能优化终极指南:memo、useCallback、useMemo全解析
前端·react.js·性能优化·usecallback·usememo·react.memo·react devtools
JuneXcy2 小时前
leetcode933最近的请求次数
开发语言·javascript·ecmascript
2301_781668618 小时前
前端基础 JS Vue3 Ajax
前端
上单带刀不带妹9 小时前
前端安全问题怎么解决
前端·安全
Fly-ping9 小时前
【前端】JavaScript 的事件循环 (Event Loop)
开发语言·前端·javascript
SunTecTec9 小时前
IDEA 类上方注释 签名
服务器·前端·intellij-idea
在逃的吗喽10 小时前
黑马头条项目详解
前端·javascript·ajax
袁煦丞10 小时前
有Nextcloud家庭共享不求人:cpolar内网穿透实验室第471个成功挑战
前端·程序员·远程工作