在这里插入图片描述

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

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

“本地跑得好好的,一上线就 403?”
“后端说接口通了,前端却报 CORS error?”
“我把 Access-Control-Allow-Origin 设成 * 怎么还是报错?”
如果你曾蹲在屏幕前对着控制台里那一串红色 blocked by CORS policy 抓狂到薅头发,别慌——这篇长文就是给你写的。读完它,你不仅能跟后端同学谈笑风生,还能在面试官面前把跨域讲成脱口秀。


为什么你写的接口总被浏览器拦下来?

故事得从浏览器的小心眼说起。
假设你正在本地 http://localhost:5173 愉快地调试 React 项目,页面里一行 fetch('https://api.xxx.com/user') 把请求甩给后端。后端秒回 200,Network 面板却一片猩红:

Access to fetch at 'https://api.xxx.com/user' from origin 'http://localhost:5173'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header…

翻译成人话:浏览器把后端的响应截胡了——“对方给你寄了快递,但小区保安(浏览器)觉得寄件人不在白名单,直接丢垃圾桶。”
跨域问题从来不是后端“没接好”,而是浏览器在中间加戏。
理解这一点,你就赢了一半。


什么是跨域?从同源策略说起

先背口诀:协议、域名、端口,仨兄弟有一个对不上,就叫跨域。

当前页面 URL请求目标 URL是否跨域原因
http://localhost:5173http://localhost:3000✅端口不同
https://a.comhttp://a.com✅协议不同
https://a.comhttps://b.com✅域名不同
https://a.comhttps://a.com/api❌三者完全一致

浏览器为什么要这么龟毛?
设想一下,如果没有限制,你点进一个钓鱼站 evil.com,它就能用 JS 偷偷请求 mail.qq.com 的接口,拿到你的邮件列表——细思极恐。
于是,同源策略(Same-Origin Policy) 诞生:默认禁止跨源读取资源。
注意,SOP 不是禁止请求,而是禁止“读”响应。请求其实发出去了,后端也回了,但浏览器不给脚本看。
就像你暗恋对象回了你微信,但手机被爸妈收了,你死活看不到内容——干着急。


浏览器眼中的“安全边界”:同源策略详解

同源策略管得有多宽?

  1. DOM 层面:禁止跨域 iframe 读写内容。
  2. Cookie/Storage:b.com 拿不到 a.com 的 localStorage。
  3. 网络层面:XHR/fetch 跨域读响应需要服务器“点头”。

例外也是有的:

  • <script src="…">、<img src="…">、<link href="…"> 这些标签可以拉跨域资源,但浏览器不允许你脚本读取返回内容。
  • 表单提交可以跨域,但同样读不到响应——早期黑客玩 “CSRF” 就靠这招。

一句话:浏览器把“写”放行,把“读”锁死。
CORS 则是官方给出的“开锁”流程。


CORS 到底是什么?一文讲清它的来龙去脉

CORS(Cross-Origin Resource Sharing)翻译过来叫“跨域资源共享”,W3C 2014 年定稿。
核心思想:服务器通过响应头告诉浏览器“我允许哪个源访问我”。
只要后端在 HTTP 响应里加几行 HTTP 头,浏览器就放行走人。

最简化的流程:

  1. 浏览器自动在请求里带上 Origin: https://a.com。
  2. 服务器返回 Access-Control-Allow-Origin: https://a.com(或 *)。
  3. 浏览器比对,一致就放行,不一致就拦截。

听起来像查身份证,对上了就过闸机。
但真实世界没这么简单——复杂请求还要先过安检(预检)。


预检请求(Preflight)是怎么回事?别再被 OPTIONS 请求吓到

先记住一句话:“非简单请求”会先发一次 OPTIONS 探路。

什么叫“简单请求”?必须同时满足以下所有条件:

  • 方法只能是 GET/HEAD/POST
  • 手动设置的请求头只能是下面 9 个之一:Accept、Accept-Language、Content-Language、Content-Type(且值只能是 application/x-www-form-urlencoded、multipart/form-data、text/plain)
  • 不使用 XMLHttpRequestUpload 或 ReadableStream
  • 不设置 withCredentials = true(后面讲)

一旦越界,浏览器就自动在真实请求前多发送一次 OPTIONS,俗称“预检”。
OPTIONS 请求里带三个关键头:

Origin: https://a.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header

服务器必须回答:

Access-Control-Allow-Origin: https://a.com
Access-Control-Allow-Methods: PUT, DELETE
Access-Control-Allow-Headers: X-Custom-Header

浏览器满意了,才真正发 PUT。
很多后端同学第一次看见 OPTIONS 404,以为是鬼,其实是预检没接。


前端代码里常见的 CORS 错误长什么样?

把日常踩过的坑整理成段子,方便你对号入座:

控制台原话人话翻译
No ‘Access-Control-Allow-Origin’ header后端忘了加白名单
The ‘Access-Control-Allow-Origin’ header contains multiple values后端重复加了头,逗号拼接
Credentials flag is true, but Access-Control-Allow-Origin is ‘*’带 cookie 时不能用 *,必须指定明确域名
Method PUT is not allowed by Access-Control-Allow-Methods预检没过,后端没配 PUT
header X-Token is not allowed后端没把自定义头写进 Allow-Headers

开发阶段如何快速绕过 CORS 限制(但别带到生产!)

本地热更新急如火,谁有空天天求后端配 CORS?
下面几招“临时歪门邪道”,仅限开发自测,上线前务必改掉。

  1. 浏览器插件
    Moesif CORS、Allow CORS: Access-Control-Allow-Origin,一键开关,原理是拦截响应强行改头。
    缺点:团队每人都要装,CI 跑不了。

  2. Chrome 启动参数

    chrome --disable-web-security --user-data-dir=/tmp/foo
    

    直接关同源策略,用完记得关,别把网银也跑进去。

  3. 本地代理(推荐)
    把 /api 转发到后端,浏览器以为同源。后面单独讲。


后端配合才是王道:CORS 响应头该怎么配?

无论 Java、Node、Go、Python,原理一样:
在 HTTP 响应里加头。下面给出最常见的三种语言配置,复制即可用。

Node + Express

// server.js
import express from 'express';
import cors from 'cors';

const app = express();

// 精细化配置
const allowList = ['http://localhost:5173', 'https://app.xxx.com'];
const corsOptions = {
  origin: (origin, cb) => {
    // 注意:postman 不带 origin,origin 为 undefined
    if (!origin || allowList.includes(origin)) cb(null, true);
    else cb(new Error('Not allowed by CORS'));
  },
  credentials: true, // 允许 cookie
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'X-Token'],
  optionsSuccessStatus: 200, // 让老 IE 开心
};

app.use(cors(corsOptions));

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

app.listen(3000, () => console.log('Server on 3000'));

Spring Boot(Kotlin DSL 版)

@Configuration
class CorsConfig : WebMvcConfigurer {
    override fun addCorsMappings(registry: CorsRegistry) {
        registry.addMapping("/api/**")
            .allowedOrigins("http://localhost:5173", "https://app.xxx.com")
            .allowedMethods("*")
            .allowedHeaders("*")
            .allowCredentials(true)
            .maxAge(3600) // 预检缓存一小时
    }
}

Nginx 反向代理(不改动后端代码)

server {
  listen 80;
  server_name api.xxx.com;

  location / {
    # 业务后端
    proxy_pass http://127.0.0.1:8080;

    # 关键 CORS 头
    add_header Access-Control-Allow-Origin $http_origin always;
    add_header Access-Control-Allow-Credentials true always;
    add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS' always;
    add_header Access-Control-Allow-Headers 'DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,X-Token' always;

    # 预检直接返回 204
    if ($request_method = OPTIONS) {
      return 204;
    }
  }
}

代理服务器怎么搭?Vite、Webpack Dev Server 实战配置

开发阶段最干净的做法:把代理配在构建工具里,前端代码完全无感知。

Vite(vite.config.ts)

import { defineConfig } from 'vite';

export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'https://api.xxx.com',
        changeOrigin: true, // 关键,把 Host 头也改掉
        secure: false,      // 若后端 https 自签证书
        rewrite: path => path.replace(/^\/api/, '/api/v1'), // 路径重写
      },
      // 如果后端给的是 cookie,记得配 sameSite
      '/upload': {
        target: 'https://api.xxx.com',
        changeOrigin: true,
      },
    },
  },
});

前端代码里直接:

// 浏览器以为是同源,毫无心理负担
fetch('/api/user').then(res => res.json());

Webpack 5(webpack.dev.js)

devServer: {
  port: 5173,
  proxy: {
    '/api': {
      target: 'https://api.xxx.com',
      changeOrigin: true,
      pathRewrite: { '^/api': '/api/v1' },
      onProxyReq: (proxyReq, req) => {
        // 若需要自定义头
        proxyReq.setHeader('X-Real-IP', req.socket.remoteAddress);
      },
    },
  },
},

线上环境 CORS 配置踩坑实录:缓存、凭证、通配符那些事

  1. 缓存坑
    浏览器对预检结果会缓存 Access-Control-Max-Age 秒数。
    如果你刚改完 Nginx 配置,用户仍用旧缓存,重启浏览器都没用。
    解决:临时把 Max-Age 设 0,或者给路径加随机 query 冲掉缓存。

  2. 凭证坑
    一旦前端 fetch(url, { credentials: 'include' }),
    后端必须返回 Access-Control-Allow-Credentials: true,
    同时 不能用 * 通配源,必须指定明确 https://a.com。
    否则浏览器直接报:

Credential is not supported if the CORS header 'Access-Control-Allow-Origin' is '*'
  1. 通配符坑
    Access-Control-Allow-Origin: * 看着爽,但:
    • 不能和 Allow-Credentials: true 同时出现;
    • 不能用在 Allow-Headers、Allow-Methods 里(需明确列出)。
      生产环境建议白名单数组,别把公司内网接口直接 * 出去。

当 Access-Control-Allow-Origin 遇上 * 和 credentials 时的冲突解法

前端诉求:
“我要带 cookie,又要允许所有域名接入。”
浏览器规则:
*带凭证时不能用 ,必须明确源。

折中方案:

  • 后端取 Origin 头,动态回写:
    const origin = req.headers.origin;
    if (allowList.includes(origin)) {
      res.setHeader('Access-Control-Allow-Origin', origin);
      res.setHeader('Access-Control-Allow-Credentials', 'true');
    }
    
  • 如果你做的是 SDK,需要被任意网站嵌入,考虑剥离凭证,改用 token 放在 Authorization 头,这样又能用 *。

排查 CORS 问题的黄金三步法:看控制台、抓包、比对响应头

  1. 看控制台
    复制报错全文,先判断是 预检失败 还是 真实请求失败。
    预检失败:OPTIONS 4xx/5xx;真实请求失败:200 但缺 CORS 头。

  2. 抓包
    打开 Network,重点看 OPTIONS 和真正的请求:

    • 有没有 Access-Control-Allow-Origin?
    • 值是否和 Origin 一致?
    • 是否返回了 Allow-Credentials、Allow-Methods、Allow-Headers?
  3. 比对响应头
    用 curl -H "Origin: http://localhost:5173" -I https://api.xxx.com/user 快速验证后端头是否吐出。
    很多框架(Spring Security、Nginx)会覆盖你的头,导致配置不生效。


实用技巧:用 fetch 和 axios 正确传递凭据和自定义头

fetch 完整示例

// 工具函数:统一封装
export async function apiGet<T>(url: string): Promise<T> {
  const res = await fetch(url, {
    method: 'GET',
    credentials: 'include', // 带 cookie
    headers: {
      'X-Token': localStorage.getItem('token') || '',
      'Content-Type': 'application/json',
    },
  });
  if (!res.ok) throw new Error(await res.text());
  return res.json();
}

// 使用
apiGet<User>('/api/user')
  .then(console.log)
  .catch(err => message.error('拉取用户失败:' + err.message));

axios 配置

import axios from 'axios';

const instance = axios.create({
  baseURL: '/api',
  timeout: 10000,
  withCredentials: true, // 关键
  headers: { 'X-Token': localStorage.getItem('token') || '' },
});

// 响应拦截器
instance.interceptors.response.use(
  res => res.data,
  err => {
    if (err.response?.status === 403) location.href = '/login';
    return Promise.reject(err);
  },
);

export default instance;

别再盲目加 proxy 了!什么场景该用 JSONP、代理还是后端改配置?

方案原理优点缺点适用场景
JSONP利用 <script> 标签无跨域限制兼容老 IE仅 GET、易 XSS万年老接口、GET 只读
后端改 CORS官方标准最干净、支持所有 HTTP 动词需后端配合新项目、自家接口
代理同源转发前端可控、开发零沟通多一次转发、线上需 Nginx第三方接口、临时过渡

一句话:能改后端就改后端,不能改就代理,JSONP 留给考古队。


CORS 安全陷阱:小心暴露敏感头信息或开放过多权限

  1. 反射 Origin 不带白名单
    攻击者用 evil.com 发请求,后端直接把 Allow-Origin 回写成 evil.com,相当于任意域放行。
    解决:永远用白名单数组 includes,别偷懒。

  2. 滥放 Allow-Headers
    把 Authorization、X-Sign 都放过去,结果前端被 XSS 后,黑客可直接拿到 Authorization 头。
    解决:只暴露业务必须的头,敏感操作再额外鉴权。

  3. Max-Age 过长
    预检缓存 86400 秒,后端想紧急关闭某域名,结果用户浏览器一周内仍认为“允许”。
    解决:线上默认缓存 1 小时以内,紧急时下线域名立刻改配置。


真实项目中如何设计前后端协作流程避免 CORS 反复出问题

  1. 项目初始化就定“源”
    开发、测试、预发、生产四环境的域名写进 README,后端直接配进代码。
    用基础设施即代码(Terraform、K8s ConfigMap)统一管理,禁止上线前临时手动加域名。

  2. Mock 优先
    前端用 MSW、YApi、Apifox 先跑 Mock,等后端 Ready 再切代理,把 CORS 问题推迟到联调阶段,避免阻塞写页面。

  3. 接口网关统一收口
    把 CORS 配在网关层(Nginx、Kong、APISIX),业务服务无感,想加域名只改网关,降低沟通成本。

  4. CI 门禁
    写一条自动化测试:用 cy.request 直接调预检,断言 204,防止后端手滑把 OPTIONS 干成 404。


你以为 CORS 解决了就万事大吉?这些边缘情况照样让你掉头发

  1. 302 重定向
    预检请求 302 跳走,浏览器直接终止后续真实请求。
    解决:让后端把 OPTIONS 原地返回 204,别重定向。

  2. 非简单端口
    比如 Electron 里用 file:// 协议打开页面,再调 http://localhost 也算跨域,且没有 Origin 头。
    解决:Electron 关 webSecurity 或用 IPC 通信。

  3. Service Worker 拦截
    SW 里 fetch(event.request) 如果改掉了 Origin,浏览器会再检查一次 CORS,导致无限预检。
    解决:SW 里克隆请求时别动 mode: 'cors' 相关字段。

  4. 混合内容(Mixed Content)
    页面 https 却请求 http 接口,浏览器先拦 Mixed Content,CORS 报错都来不及出。
    解决:全站 HTTPS,或在 Nginx 反向代理把 HTTP 升级。


彩蛋:一行代码让全站接口永远告别 CORS(伪)

// 仅用于愚人节整蛊同事
fetch = () => Promise.reject(new Error('CORS ERROR 🤡'));

写在最后

跨域问题就像感冒,症状看着吓人,病因其实就那几个。
记住“浏览器才是大 BOSS,后端点头就放行”,再配好预检、凭证、白名单三板斧,基本就能药到病除。
下次再看到控制台飘红,别急着把 * 怼满全站,先深呼吸——
打开 Network,看看 OPTIONS 回了啥,比转发锦鲤管用多了。

愿你今后写接口,只有 200,没有 CORS;
愿你和后端同学因为一篇文章,少吵三天架;
愿世间不再有不眠之夜,只为那一行 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等工具

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

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

在这里插入图片描述

更多推荐