前端开发者必看:彻底搞懂CORS跨域问题(原理+实战+避坑指南)

在这里插入图片描述

前端开发者必看:彻底搞懂CORS跨域问题(原理+实战+避坑指南)

前端开发者必看:彻底搞懂CORS跨域问题(原理+实战+避坑指南)

友情提示:本文含大量代码,阅读前请备好咖啡、瓜子和防脱发洗发水。


引言:你是不是又被那个烦人的“跨域错误”拦住了?

凌晨两点,刚把新功能 push 上线,老板在群里甩了一张截图:

Access to fetch at 'https://api.xxx.com' from origin 'https://h5.xxx.com' has been blocked by CORS policy...

你揉揉惺忪睡眼,内心 OS:
“我代码明明没动,怎么又跨域了?!”

别慌,今天咱们就把 CORS 这货按在地上摩擦——从浏览器出生说起,到生产环境收官,顺带把坑填平。读完本文,你将获得:

  • 一张“跨域”全景地图
  • 一套“从开发到上线”的实操锦囊
  • 一箩筐“别人踩过你也别踩”的血泪案例

准备好?那就把键盘翻过来,抖一抖饼干渣,开干!


跨域到底是个啥?从浏览器安全说起

故事要从 1995 年讲起。那年 JavaScript 刚出生,网景工程师为了防止某个邪恶网页偷偷把你的银行余额给读出来,搞了个“同源策略”:

协议、域名、端口,仨哥们必须完全一致,否则浏览器就掀桌子。

举个例子:

页面地址请求地址是否同源原因
https://a.com:443https://a.com/api一模一样
https://a.comhttp://a.com协议不同
https://a.comhttps://b.com域名不同
https://a.com:443https://a.com:8080端口不同

浏览器就像固执的保安:
“证件不符?对不起,枪毙。”

可现实总让人啪啪打脸——前后端分离已成标配,页面和接口常常不在一个域。于是 W3C 老大哥站出来:
“别动不动就枪毙,咱走个流程,协商一下。”

这个“协商流程”就是 CORS(Cross-Origin Resource Sharing,跨域资源共享)。


CORS机制全解析:浏览器和服务器是怎么“商量”跨域的

CORS 本质上是浏览器与服务器的一次暗号对接。流程分两种:

  1. 简单请求(Simple Request)
  2. 非简单请求(Preflighted Request)

简单请求:浏览器“放行条”直接过

满足以下所有条件,就是简单请求:

  • 方法:GET、HEAD、POST 之一
  • 头:仅接受 CORS 安全集合(如 AcceptContent-Type: text/plain 等)
  • 无自定义头
  • 请求体只支持三种 Content-Typetext/plainmultipart/form-dataapplication/x-www-form-urlencoded

此时浏览器只在请求头里悄悄塞一个:

Origin: https://h5.xxx.com

服务器如果愿意,就回一个:

Access-Control-Allow-Origin: https://h5.xxx.com

浏览器一看:“暗号对上了,过!”

非简单请求:先“发一封邮件”再进门

一旦用了 PUTDELETE,或者自定义头,浏览器就会先发一次 OPTIONS 预检,就像快递小哥先打电话:“有人在家吗?”

预检请求长这样:

OPTIONS /api/upload HTTP/1.1
Host: api.xxx.com
Origin: https://h5.xxx.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-Custom-Token

服务器回:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://h5.xxx.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-Custom-Token
Access-Control-Max-Age: 86400

浏览器收到“准奏”后,才真正发业务请求。

这就是“为什么有时发两次请求”的原因——第一次 OPTIONS 是探路,第二次才是真身


预检请求(Preflight)到底在干啥?为什么有时候发两次请求?

把预检想象成过安检

  • 你带了一把瑞士军刀(自定义头),安检员要先看看能不能带。
  • 你说“我就进去 5 分钟”(Max-Age),安检员记住你,今天之内不再搜身。

代码层面,如何减少预检?

  1. 尽量用简单请求
  2. 别把 Content-Type 写成 application/json——一写就预检
  3. 让后端把 Access-Control-Max-Age 调大,浏览器会缓存预检结果,86400 秒(24h) 不重复安检

前端常见的跨域场景大盘点:开发、联调、上线全阶段

阶段典型地址跨域?常见症状
本地开发localhost:3000localhost:8080✅ 端口不同控制台一片红
本地联调localhost:3000dev.api.com✅ 域名不同预检 405
测试环境https://test.xxx.comhttps://api.xxx.com✅ 子域不同带 cookie 失败
生产环境https://h5.xxx.comhttps://api.xxx.com被老板 @

开发环境下的跨域处理妙招:代理、插件、本地服务怎么选

1. Webpack DevServer 代理(最香)

// vue.config.js 或 webpack.config.js
module.exports = {
  devServer: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true, // 关键:把请求头里的 Host 改掉
        pathRewrite: { '^/api': '' }, // 可选:去掉前缀
      },
    },
  },
};

前端代码毫无感知:

// 像调用同源接口一样
fetch('/api/user/info')
  .then(res => res.json())
  .then(console.log);

代理就像“代购”:浏览器以为你在买国内现货,其实服务器偷偷去海外下单。

2. Vite 代理(更香)

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

export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/api/, ''),
      },
    },
  },
});

3. Chrome 插件 Moesif CORS(临时救急)

只能本地调试用,原理是屏蔽浏览器校验,上线无效
安装后点一下“ON”,小绿标亮起,世界安静了。

友情提示:千万别让测试同学装了这个插件,不然 BUG 全在测试环境隐身,上线集体翻车。

4. 本地 Nginx 反向代理(全能型)

# /usr/local/etc/nginx/nginx.conf (mac)
server {
  listen 80;
  server_name local.fe.com;

  location / {
    proxy_pass http://localhost:3000; # 前端本地服务
  }

  location /api {
    proxy_pass http://localhost:8080; # 后端服务
    proxy_set_header Host $host;
  }
}

hosts 文件加一行:

127.0.0.1 local.fe.com

浏览器访问 http://local.fe.com完美同源,连 HTTPS 都能模拟。


生产环境跨域实战:如何配合后端正确配置Access-Control-Allow-Origin

场景 1:只允许固定域名

# Nginx
add_header Access-Control-Allow-Origin https://h5.xxx.com always;
add_header Access-Control-Allow-Credentials true always;

必须加 always,否则 4xx/5xx 时 Nginx 默认不返回 CORS 头,前端拿不到错误详情,调试原地爆炸。

场景 2:允许多个域名(动态)

// Node.js (Express)
const ALLOW_MAP = {
  'https://h5.xxx.com': 1,
  'https://www.xxx.com': 1,
};

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (ALLOW_MAP[origin]) {
    res.header('Access-Control-Allow-Origin', origin);
    res.header('Access-Control-Allow-Credentials', 'true');
  }
  next();
});

场景 3:全站放开(不推荐)

add_header Access-Control-Allow-Origin * always;

一旦带cookie(credentials: 'include'),浏览器会直接枪毙,因为 *credentials 互斥。


那些年我们踩过的CORS坑:凭据、自定义头、状态码的隐藏雷区

坑 1:带 Cookie 没加 credentials

前端:

fetch('https://api.xxx.com/user/info', {
  credentials: 'include', // 必须显式声明
});

后端:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://h5.xxx.com  // 不能是 *

少一个字符,cookie 就隐身

坑 2:自定义头漏写 Access-Control-Allow-Headers

前端:

fetch('https://api.xxx.com/upload', {
  method: 'POST',
  headers: {
    'X-Upload-Id': '12345',
  },
});

后端只回了:

Access-Control-Allow-Headers: Content-Type

浏览器直接报错:

Request header field X-Upload-Id is not allowed by Access-Control-Allow-Headers

正确姿势:后端把所有自定义头都写进去,或用白名单动态拼接。

坑 3:非 2xx 状态码被浏览器吞

后端返回 401 并附带 JSON:

{ "code": 401001, "msg": "Token 过期" }

但 Nginx 没加 always,导致 CORS 头丢失,浏览器连响应体都不给前端看fetch 直接进 catch拿不到 msg
前端一脸懵:“到底错在哪?”

解决方案

  1. Nginx 加 always
  2. 后端在 2xx 里包自定义 code,别滥用 HTTP 状态码

遇到跨域报错别慌:一步步排查问题的实用思路

  1. 看控制台
    复制第一条红色报错,全文搜索 CORS,找到关键词 blocked by CORS policy

  2. 确认触发场景

    • 开发环境?先检查代理是否生效
    • 生产环境?抓包看响应头
  3. 抓包对比

    • 没收到 OPTIONS → 简单请求,重点看 Access-Control-Allow-Origin
    • OPTIONS 405 → 后端没允许 OPTIONS 方法,Nginx 把 OPTIONS 当静态文件了
  4. 检查响应头
    curl -H "Origin: https://h5.xxx.com" -I https://api.xxx.com/user 迅速验证

  5. 最小复现
    新建一个 test.html,只留一段 fetch,排除业务代码干扰:

<!doctype html>
<html>
  <body>
    <script>
      fetch('https://api.xxx.com/user', { credentials: 'include' })
        .then(res => res.json())
        .then(console.log)
        .catch(console.error);
    </script>
  </body>
</html>
  1. 逐级定位
    代理 → Nginx → 网关 → 应用,每一层都可能把头吞掉,一层一层砍,直到看到可爱的 Access-Control-Allow-Origin 出现为止。

让跨域不再头疼的前端技巧:封装请求、统一拦截、错误兜底

1. 封装 Axios 实例

// src/utils/request.js
import axios from 'axios';
import { getToken } from './auth';
import { Toast } from 'antd-mobile';

// 创建实例
const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE, // 自动区分环境
  timeout: 15000,
  withCredentials: true, // 全局带 cookie
});

// 请求拦截
service.interceptors.request.use(
  config => {
    const token = getToken();
    if (token) {
      config.headers['X-Token'] = token; // 统一注入
    }
    return config;
  },
  error => Promise.reject(error)
);

// 响应拦截
service.interceptors.response.use(
  response => {
    const { data } = response;
    // 后端自定义 code
    if (data.code !== 0) {
      Toast.show({
        content: data.msg || '系统繁忙',
        icon: 'fail',
      });
      return Promise.reject(new Error(data.msg));
    }
    return data.data;
  },
  error => {
    // 网络 / CORS / 超时
    if (error.response?.status === 401) {
      // 清 token 跳登录
      location.href = '/login';
    } else {
      Toast.show({
        content: error.message || '网络开小差',
        icon: 'fail',
      });
    }
    return Promise.reject(error);
  }
);

export default service;

业务层直接:

import request from '@/utils/request';

export function getUserInfo() {
  return request({ url: '/user/info', method: 'get' });
}

2. 统一错误上报

// 在响应拦截器里加一行
window.$sentry?.captureException(error);

把 CORS 错误直接送进 Sentry,生产环境第一时间收到邮件,比用户截图快 10 倍。

3. 接口容灾兜底

// 针对重要接口做本地缓存
const CACHE_KEY = 'user_info_backup';
getUserInfo()
  .then(data => {
    localStorage.setItem(CACHE_KEY, JSON.stringify(data));
    return data;
  })
  .catch(() => {
    // 跨域失败?拿缓存顶一顶
    const cache = localStorage.getItem(CACHE_KEY);
    if (cache) return JSON.parse(cache);
    throw new Error('网络不可用,请检查设置');
  });

你以为CORS只是后端的事?前端也能主动“助攻”跨域配置

1. 提前给后端“打预防针”

接口评审时,把需要跨域的域名、自定义头、是否带cookie 写成文档甩过去,别让后端哥哥猜

接口方法自定义头cookie域名
/user/infoGETX-Tokenhttps://h5.xxx.com
/uploadPOSTX-Upload-Idhttps://h5.xxx.com

2. 用 Node 中间层“代理”转发(BFF)

前端自己搭一个 Serverless 函数,把跨域问题收敛到内部:

// vercel/api/proxy.js
export default async (req, res) => {
  const { path, ...query } = req.query;
  const realUrl = `https://api.xxx.com/${path}`;
  const result = await fetch(realUrl, {
    method: req.method,
    headers: {
      ...req.headers,
      host: 'api.xxx.com',
    },
    body: req.method === 'GET' ? undefined : JSON.stringify(req.body),
  });
  const data = await result.json();
  res.status(result.status).json(data);
};

前端调用:

fetch('/api/proxy?path=user/info')

浏览器看到的是同源,跨域?不存在的。

3. 把静态资源放 CDN 时加 crossorigin 属性

<script
  src="https://cdn.xxx.com/lib/vue.global.prod.js"
  crossorigin="anonymous"
></script>

防止 window.onerror 拿不到详细报错信息,Sentry 只显示 Script Error.


彩蛋:一行代码让 OPTIONS 消失?

// 仅适用于开发环境,把非简单请求降级成简单请求
// 把 JSON 改成 form-urlencode
const params = new URLSearchParams();
params.append('name', 'json');
params.append('age', '18');
fetch('/api/user', {
  method: 'POST',
  body: params,
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
});

后端配合改解析:

// Node.js
app.use(express.urlencoded({ extended: true }));

缺点:只能传字符串,文件上传别玩这招。


收个尾

跨域就像青春期痘痘——不请自来,反复横跳
搞懂原理、备好工具、提前沟通、留好兜底,才能让它少冒出来

下次再看到 blocked by CORS policy,别急着抓耳挠腮,打开这篇文章,按图索骥,刀刀见肉

祝你早日和跨域说分手,夜班不再被老板 @,发量依旧坚挺。

如果本文帮到了你,记得把链接甩给还在踩坑的队友——独秃秃不如众秃秃

欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。

推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!


专栏系列(点击解锁)学习路线(点击解锁)知识定位
《微信小程序相关博客》 持续更新中~结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等
《AIGC相关博客》 持续更新中~AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结
《HTML网站开发相关》 《前端基础入门三大核心之html相关博客》前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识
《前端基础入门三大核心之JS相关博客》前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。
通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心
《前端基础入门三大核心之CSS相关博客》介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页
《canvas绘图相关博客》Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化
《Vue实战相关博客》持续更新中~详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅
《python相关博客》持续更新中~Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具
《sql数据库相关博客》持续更新中~SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能
《算法系列相关博客》持续更新中~算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维
《IT信息技术相关博客》持续更新中~作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识
《信息化人员基础技能知识相关博客》无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方
《信息化技能面试宝典相关博客》涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面
《前端开发习惯与小技巧相关博客》持续更新中~罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等
《photoshop相关博客》 持续更新中~基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结
日常开发&办公&生产【实用工具】分享相关博客》持续更新中~分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具

吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DTcode7

客官,赏个铜板吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值