前端开发者必读:彻底搞懂CORS跨域问题与实战解决方案

在这里插入图片描述

前端开发者必读:彻底搞懂CORS跨域问题与实战解决方案

——“哥,本地跑得好好的,一上线就 403,浏览器是不是跟我有仇?”
——“不,它只是比你更懂安全。”

引言:为什么你的请求总被浏览器“拒之门外”?

凌晨两点,刚把新功能推到预发,老板在群里甩了一张截图:
Access to XMLHttpRequest at 'https://api.xxx.com' from origin 'https://admin.xxx.com' has been blocked by CORS policy.
一句话,让无数前端瞬间清醒。
别急着骂浏览器矫情,它其实是个尽职尽责的保安:没有“通行证”(CORS 头),外校人员(跨域请求)一律不准进。
本文就带你从保安的视角、后端同事的视角、甚至“奸商”视角(黑客)重新审视 CORS,顺便塞给你一麻袋代码,保你以后见到“blocked by CORS policy”不再头皮发麻。

揭开 CORS 的神秘面纱:从同源策略说起

故事要从 1995 年说起,网景工程师写了几行代码,决定“端口不同就算异族”。
所谓同源,指协议 + 域名 + 端口三件套完全一致。
只要有一个字符不对,浏览器就会把你的响应包丢进黑洞,页面拿不到任何数据——这就是同源策略
它像极了我妈不让我跟隔壁班“坏孩子”玩:
“人家成绩差(不同源),别去招惹!”
可业务偏偏要“联姻”:前端跑在 https://app.xxx.com,接口在 https://api.xxx.com,端口不同,当场被棒打鸳鸯。
于是 W3C 站出来当和事佬:
“别急着拆散,让服务器给孩子发个通行证(CORS 头)就行。”
通行证长啥样?后文慢慢拆。

CORS 到底是什么?用生活场景讲清楚跨域资源共享机制

把浏览器想象成“超大型机场”,安检口写着:
“凡从本地起飞(Origin)前往外站的航班,必须出示目的站签发的登机牌(Access-Control-Allow-Origin)。”
登机牌可以是:

  1. 精准登机牌:Access-Control-Allow-Origin: https://app.xxx.com
  2. wildcard 登机牌:Access-Control-Allow-Origin: *
  3. 动态登机牌:后端先瞅一眼你的身份证(Origin 头),再决定给不给你盖章。
    一旦盖章,安检就放行;不盖章,直接遣返,还会贴心地附赠一条报错信息,生怕你不知道被谁拒了。
    看到这里,聪明的你一定悟了:CORS 的本质是“服务器告诉浏览器,我愿意被谁跨域”
    浏览器只负责检票,不办证,办证得找后端——这也是 90% 的跨域问题最后都变成“后端大哥,加个头”的原因。

浏览器眼中的 CORS:预检请求、简单请求与实际请求的区别

浏览器内部有两条通道:
绿色通道(简单请求):

  • 方法只能是 GET/HEAD/POST
  • 手动设置的请求头只能出现 AcceptAccept-LanguageContent-LanguageContent-Type,且 Content-Type 值只能是 application/x-www-form-urlencodedmultipart/form-datatext/plain
  • 没有上传任何 ReadableStreamBlob 等花哨东西
    满足以上全部,浏览器觉得“这哥们人畜无害”,直接发请求,再在响应头里检查登机牌。
    红色通道(非简单请求):
    比如你想带 JSON 上路,或者自定义头 X-Requested-With,浏览器立马紧张:
    “兄弟,你这包裹里怕不是有液体?”
    于是先派一架无人机(OPTIONS 预检)探路,无人机背上写着:
    “我是预检,后面那架大飞机(真实请求)想带 Content-Type: application/json,你批不批?”
    服务器必须在 OPTIONS 阶段返回:
Access-Control-Allow-Origin: https://app.xxx.com  
Access-Control-Allow-Methods: POST, GET, OPTIONS  
Access-Control-Allow-Headers: Content-Type  

无人机回去复命:“放行!” 浏览器才发真正的 POST。
一旦预检被拒,真实请求直接胎死腹中,报错信息里会出现 ...CORS policy: Response to preflight request... 字样。
记住:看到 preflight 关键词,先去抓 OPTIONS。

后端配合有多关键?常见 CORS 响应头字段详解

以下头字段,前端最好背下来,省得甩锅时支支吾吾:

  • Access-Control-Allow-Origin(ACAO):通行证正面,可以是具体源,也可以是 *
  • Access-Control-Allow-Credentials(ACAC):是否允许携带 Cookie/Authorization,值只能是 true不能和 * 同时出现
  • Access-Control-Allow-Methods:预检时返回,告诉浏览器后续允许哪些方法。
  • Access-Control-Allow-Headers:预检时返回,允许携带的自定义头,多个用逗号分隔。
  • Access-Control-Max-Age:预检缓存时间,单位秒,可减少 OPTIONS 往返。
  • Access-Control-Expose-Headers:默认浏览器只能拿到 Cache-ControlContent-Language 等 6 个“安全头”,如果想让前端读取 X-Total-Count,必须加这个头。

Node(Express)最小可运行示例:

// server.js
const express = require('express');
const cors = require('cors');
const app = express();

// 1. 简单粗暴版 —— 允许任何源,但无法带 Cookie
// app.use(cors());

// 2. 精细控制版 —— 动态判断 Origin
const allowlist = ['https://app.xxx.com', 'https://m.xxx.com'];
const corsOptions = {
  origin: (origin, callback) => {
    // 注意:移动端壳子或 Postman 发请求时 origin 可能是 undefined
    if (!origin || allowlist.includes(origin)) {
      callback(null, origin); // 把具体源回传,浏览器才能收到
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true, // 关键,允许 Cookie
  optionsSuccessStatus: 200 // 部分旧浏览器对 204 支持不好
};
app.use(cors(corsOptions));

app.get('/api/user', (req, res) => {
  res.json({ name: 'cors', age: 18 });
});

app.listen(3000, () => console.log('api run at 3000'));

把上面文件 node server.js 跑起来,前端再用 fetch(..., {credentials:'include'}) 就能愉快带 Cookie 了。
如果后端是 Java,SpringBoot 一把梭:

@Configuration
public class CorsConfig {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**")
                        .allowedOriginPatterns("https://*.xxx.com") // Spring 5.3+
                        .allowCredentials(true)
                        .allowedMethods("*")
                        .allowedHeaders("*")
                        .maxAge(3600);
            }
        };
    }
}

注意 Spring 低版本没有 allowedOriginPatterns,只能写死 allowedOrigins("https://app.xxx.com")不能用 * 同时开启凭证,否则会抛异常。

前端发起跨域请求的正确姿势:fetch 与 axios 中的注意事项

fetch 示例:

// 带 JSON 但不带 Cookie
fetch('https://api.xxx.com/api/user', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ id: 1 })
});

// 带 Cookie(credentials 必须手动开)
fetch('https://api.xxx.com/api/user', {
  credentials: 'include' // 等价于 axios withCredentials: true
});

axios 示例:

import axios from 'axios';

// 全局默认
axios.defaults.withCredentials = true;
axios.defaults.baseURL = 'https://api.xxx.com';

// 单独请求覆盖
axios.post('/login', { user: 'tom' }, { withCredentials: false });

踩坑点:

  1. 一旦开启 withCredentials=true后端必须返回具体源,不能是 *,否则浏览器直接拒收。
  2. 自定义头大小写不敏感,但写错一个字母,预检就过不了。
  3. axios.interceptors.response.use 里捕获到的 error,若因为 CORS 被拒绝,status 拿不到,只能看到 message: "Network Error",别傻傻地 switch(error.response.status),会炸。

开发环境下的 CORS 临时绕过技巧

场景一:本地起了一个 create-react-app,端口 3000,后端在 8080,后端大哥请假旅游。
方案 A:CRA 自带的 proxy
package.json 里加一行:

"proxy": "http://localhost:8080"

CRA 会把所有未知路由(非静态资源)转发到 8080,浏览器看到的还是 3000,同源,完美绕过。
如果想代理 WebSocket 或加路径过滤,用 src/setupProxy.js

const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = app => {
  app.use('/api',
    createProxyMiddleware({
      target: 'http://localhost:8080',
      changeOrigin: true,
      ws: true
    })
  );
};

方案 B:Vite 配置
vite.config.ts

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

方案 C:Nginx 本地反向代理

server {
    listen 80;
    server_name local.test;
    location /api {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
    }
    location / {
        proxy_pass http://localhost:3000;
    }
}

方案 D:浏览器插件
Moesif CORS 一键开关,仅推荐调试自用,上线前记得关,否则客户会收到“您的网站在裸奔”锦旗。

生产环境中如何优雅处理 CORS?

  1. 网关层统一收口
    把 CORS 配置下沉到网关(Nginx、Kong、Traefik),业务服务无感。
    Nginx 示例:

    map $http_origin $cors_origin {
        ~^https://(.*)\.xxx\.com$ $http_origin;
        default "";
    }
    server {
        listen 443 ssl;
        server_name api.xxx.com;
        location / {
            if ($request_method = 'OPTIONS') {
                add_header Access-Control-Allow-Origin $cors_origin;
                add_header Access-Control-Allow-Credentials true;
                add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
                add_header Access-Control-Allow-Headers '$http_access_control_request_headers';
                add_header Access-Control-Max-Age 86400;
                return 204;
            }
            add_header Access-Control-Allow-Origin $cors_origin always;
            add_header Access-Control-Allow-Credentials true always;
            proxy_pass http:// upstream;
        }
    }
    

    重点:always 关键字,Nginx 默认 4xx/5xx 不会带 CORS 头,前端捕获错误时可能拿不到体,造成“黑盒”。

  2. 按环境隔离
    生产域名严格白名单,测试环境可以用 *.test.xxx.com 通配,减少摩擦。

  3. 灰度发布
    新增域名先上灰度,确认通行证生效再全量,防止“上线即 403”。

踩坑实录:那些年我们遇到的 CORS 报错及背后真相

错误 1:
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is included.
真相:既想 * 又想带 Cookie,浏览器直接判你“渣男”。

错误 2:
Request header field x-token is not allowed by Access-Control-Allow-Headers in preflight response.
真相:自定义头 x-token 没加到后端白名单,OPTIONS 阶段就被拒。

错误 3:
CORS Missing Allow Origin
真相:后端忘了加 ACAO,或者 Nginx 反向代理把后端返回的头“吃”了,常见于 proxy_hide_header 写错。

错误 4:
ERR_CERT_AUTHORITY_INVALID + CORS 一起蹦
真相:HTTPS 证书过期,浏览器先报 TLS 错误,再顺带报 CORS,优先解决证书,别被带偏。

当 Access-Control-Allow-Origin 遇到通配符和凭证冲突怎么办?

口诀:“星号不搭 Cookie,凭证必配具体源”
如果业务有 200 个域名,手动写死太蠢?上“动态源白名单”:

// Node 示例
const origin = req.headers.origin;
if (await redis.sismember('cors:whitelist', origin)) {
  res.setHeader('Access-Control-Allow-Origin', origin);
  res.setHeader('Access-Control-Allow-Credentials', 'true');
}

把域名放 Redis 集合,新增客户只需写入一次,重启都不带抖。

动态设置 CORS 头的高级玩法:按域名、用户或路径灵活授权

  1. 按路径
    /public/** 允许 */private/** 必须登录,只能指定源。
    Spring Security 配置顺序:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().configurationSource(request -> {
            CorsConfiguration cfg = new CorsConfiguration();
            String path = request.getRequestURI();
            if (path.startsWith("/public")) {
                cfg.addAllowedOrigin("*");
            } else {
                cfg.addAllowedOrigin("https://app.xxx.com");
                cfg.setAllowCredentials(true);
            }
            return cfg;
        });
    }
    
  2. 按租户
    租户 A 自定义域名 https://a.com,租户 B 用 https://b.com,数据存数据库,启动时刷到内存,请求来时动态匹配,防止租户越权

  3. 按用户
    高级场景:内网管理员走 https://admin.xxx.com,普通客户走 https://app.xxx.com,根据 JWT 里的角色再二次校验,前端无感,后端加一层拦截器即可

前端也能主动“防御”:错误捕获、降级方案与用户体验优化

  1. 全局捕获

    // React 错误边界
    class CORSBoundary extends React.Component {
      state = { hasError: false };
      static getDerivedStateFromError(error) {
        if (error.message.includes('CORS')) return { hasError: true };
        throw error; // 其他错误继续抛
      }
      render() {
        return this.state.hasError
          ? <Result status="warning" title="跨域异常,请联系运维检查 CORS 配置"/>
          : this.props.children;
      }
    }
    
  2. 自动重试
    axios-retry 插件,判断 error.code === 'ERR_NETWORK'延迟 2 秒再试一次,降低因 OPTIONS 偶尔 502 造成的白屏。

  3. 日志上报
    因为 CORS 报错拿不到 status,可以把 window.onerror 里的 message、filename、lineno、colno 一并丢到 Sentry,辅助后端定位

  4. 降级方案
    关键接口走同域网关转发,非关键接口(如埋点)允许失败,防止“一颗老鼠屎坏了一锅粥”

调试 CORS 问题的实用工具箱

  1. Chrome DevTools
    Network 面板 → 选中请求 → Response Headers → 看有没有 ACAO;如果没有,立即切换到 “Issues” 标签,Chrome 会贴心提示哪一步出错。

  2. curl 模拟预检

    curl -X OPTIONS https://api.xxx.com/login \
      -H "Origin: https://app.xxx.com" \
      -H "Access-Control-Request-Method: POST" \
      -H "Access-Control-Request-Headers: Content-Type" \
      -v
    

    观察返回头,没有 ACAO 直接锤后端

  3. 在线检测
    https://www.test-cors.org 一键生成代码,还能可视化查看头,甩给测试妹妹也能看懂

  4. 抓包神器 Wireshark
    在 HTTPS 场景,配合 SSLKEYLOGFILE 环境变量解密 TLS,确认 Nginx 是否把后端返回的 ACAO 吞了

别再盲目加 header 了!理解 OPTIONS 请求的触发条件与性能影响

每一条 OPTIONS 都是一次 RTT,海外用户直接 +300ms。
减少预检次数:

  • 缓存:Access-Control-Max-Age: 86400(单位秒),24 小时内同类型请求免预检
  • 合并接口:GraphQL 一次请求扛下所有查询,减少自定义头种类
  • 降复杂度:把自定义头 X-User-Id 放到 Cookie,变“非简单”为“简单”

真实项目案例复盘

案例 1:电商大促

背景:静态页面在 CDN,接口在高防 IP,预检流量暴涨。
解法:把 Access-Control-Max-Age 提到 86400,预检 QPS 从 5k 降到 200;同时把价格接口的 Content-Type 改成 text/plain(内部协议解析),彻底消灭预检

案例 2:后台管理系统

背景:客户现场部署私有域 https://erp.customer.com,我们域名白名单写死 https://*.xxx.com,现场 403。
解法:后端提供“租户配置页”,客户自助填写域名,后端动态刷新内存缓存,无需发版。

案例 3:第三方 API 集成

背景:要调银行 SDK,银行只接受白名单域名,测试环境每天变。
解法:测试机通过 SSH 动态把 localhost 映射到 https://test.xxx.com用本地 Nginx + 自签证书,同时把自签根证书导入操作系统,浏览器零警告,CORS 正常。

写给未来自己的备忘:一份可复用的 CORS 问题排查清单

  1. 看报错关键词:

    • preflight → 抓 OPTIONS
    • wildcard * → 检查是否带 Cookie
    • not allowed by Access-Control-Allow-Headers → 自定义头未放行
  2. 快速验证:curl 模拟 OPTIONS,不带业务逻辑,纯粹检查头

  3. 确认链路:
    浏览器 → 网关 → 业务服务 → 网关 → 浏览器,每一步都可能被吞头

  4. 缓存作祟:
    后端改完头,记得清 CDN、清浏览器缓存,否则依旧 403。

  5. 上线 checklist:

    • 生产域名字段大小写
    • HTTPS 证书有效期
    • Access-Control-Max-Age 是否过大(调试时建议先设 0)
    • Sentry 是否加了网络错误采样
  6. 最后一条:永远不要把 *credentials=true 同时出现,除非你想在事故复盘会上背锅。

——完——
愿你下一次见到 CORS,不再心跳加速,而是嘴角上扬:“小样儿,通行证在我手里。”

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

推荐: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等工具

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

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

在这里插入图片描述

内容概要:本文围绕“考虑电能交互的冷热电区多微网系统双层多场景协同优化配置”的Matlab代码实现展开,提出一种结合电能交互机制的双层优化模型,用于解决冷、热、电多能耦合背景下多微网系统的协同规划运行问题。研究采用多场景分析方法应对可再生能源出力负荷需求的不确定性,通过上层规划设备容量配置下层优化多时段运行策略的联动,提升系统在复杂环境下的经济性、鲁棒性能源利用效率。所提供的Matlab代码集成了建模、求解(如YALMIP+CPLEX)结果可视化全流程,涵盖场景生成削减、双层优化结构设计及多能流协同调度等关键技术环节,为综合能源系统优化提供了完整的算法实现技术参考。; 适合人群:具备电力系统、综合能源系统或优化建模背景,熟悉Matlab编程数学规划方法,正在从事相关领科研或工程设计工作的研究生、高校研究人员及能源行业技术人员。; 使用场景及目标:①开展冷热电联供(CCHP)多微网系统的容量规划运行优化研究;②支撑含分布式能源、储能及多能转换设备的综合能源系统多目标、多场景优化建模;③学习复现双层优化、分布鲁棒优化及场景分析等先进优化方法在能源系统中的实际应用。; 阅读建议:建议结合配套文献代码同步研读,重点理解双层模型的构建逻辑、变量耦合关系求解技巧,关注场景生成方法YALMIP调用细节,通过调整参数、修改目标函数等方式进行仿真实验,以深化对系统优化机理的掌握。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

DTcode7

客官,赏个铜板吧

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

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

打赏作者

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

抵扣说明:

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

余额充值