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

前端开发者必看:彻底搞懂 CORS 跨域问题与实战解决方案
- 前端开发者必看:彻底搞懂 CORS 跨域问题与实战解决方案
- 为什么你写的接口总被浏览器拦下来?
- 什么是跨域?从同源策略说起
- 浏览器眼中的“安全边界”:同源策略详解
- CORS 到底是什么?一文讲清它的来龙去脉
- 预检请求(Preflight)是怎么回事?别再被 OPTIONS 请求吓到
- 前端代码里常见的 CORS 错误长什么样?
- 开发阶段如何快速绕过 CORS 限制(但别带到生产!)
- 后端配合才是王道:CORS 响应头该怎么配?
- 代理服务器怎么搭?Vite、Webpack Dev Server 实战配置
- 线上环境 CORS 配置踩坑实录:缓存、凭证、通配符那些事
- 当 Access-Control-Allow-Origin 遇上 * 和 credentials 时的冲突解法
- 排查 CORS 问题的黄金三步法:看控制台、抓包、比对响应头
- 实用技巧:用 fetch 和 axios 正确传递凭据和自定义头
- 别再盲目加 proxy 了!什么场景该用 JSONP、代理还是后端改配置?
- CORS 安全陷阱:小心暴露敏感头信息或开放过多权限
- 真实项目中如何设计前后端协作流程避免 CORS 反复出问题
- 你以为 CORS 解决了就万事大吉?这些边缘情况照样让你掉头发
- 彩蛋:一行代码让全站接口永远告别 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:5173 | http://localhost:3000 | ✅ | 端口不同 |
| https://a.com | http://a.com | ✅ | 协议不同 |
| https://a.com | https://b.com | ✅ | 域名不同 |
| https://a.com | https://a.com/api | ❌ | 三者完全一致 |
浏览器为什么要这么龟毛?
设想一下,如果没有限制,你点进一个钓鱼站 evil.com,它就能用 JS 偷偷请求 mail.qq.com 的接口,拿到你的邮件列表——细思极恐。
于是,同源策略(Same-Origin Policy) 诞生:默认禁止跨源读取资源。
注意,SOP 不是禁止请求,而是禁止“读”响应。请求其实发出去了,后端也回了,但浏览器不给脚本看。
就像你暗恋对象回了你微信,但手机被爸妈收了,你死活看不到内容——干着急。
浏览器眼中的“安全边界”:同源策略详解
同源策略管得有多宽?
- DOM 层面:禁止跨域 iframe 读写内容。
- Cookie/Storage:
b.com拿不到a.com的localStorage。 - 网络层面:XHR/fetch 跨域读响应需要服务器“点头”。
例外也是有的:
<script src="…">、<img src="…">、<link href="…">这些标签可以拉跨域资源,但浏览器不允许你脚本读取返回内容。- 表单提交可以跨域,但同样读不到响应——早期黑客玩 “CSRF” 就靠这招。
一句话:浏览器把“写”放行,把“读”锁死。
CORS 则是官方给出的“开锁”流程。
CORS 到底是什么?一文讲清它的来龙去脉
CORS(Cross-Origin Resource Sharing)翻译过来叫“跨域资源共享”,W3C 2014 年定稿。
核心思想:服务器通过响应头告诉浏览器“我允许哪个源访问我”。
只要后端在 HTTP 响应里加几行 HTTP 头,浏览器就放行走人。
最简化的流程:
- 浏览器自动在请求里带上
Origin: https://a.com。 - 服务器返回
Access-Control-Allow-Origin: https://a.com(或*)。 - 浏览器比对,一致就放行,不一致就拦截。
听起来像查身份证,对上了就过闸机。
但真实世界没这么简单——复杂请求还要先过安检(预检)。
预检请求(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?
下面几招“临时歪门邪道”,仅限开发自测,上线前务必改掉。
-
浏览器插件
Moesif CORS、Allow CORS: Access-Control-Allow-Origin,一键开关,原理是拦截响应强行改头。
缺点:团队每人都要装,CI 跑不了。 -
Chrome 启动参数
chrome --disable-web-security --user-data-dir=/tmp/foo直接关同源策略,用完记得关,别把网银也跑进去。
-
本地代理(推荐)
把/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 配置踩坑实录:缓存、凭证、通配符那些事
-
缓存坑
浏览器对预检结果会缓存Access-Control-Max-Age秒数。
如果你刚改完 Nginx 配置,用户仍用旧缓存,重启浏览器都没用。
解决:临时把Max-Age设 0,或者给路径加随机 query 冲掉缓存。 -
凭证坑
一旦前端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 '*'
- 通配符坑
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 问题的黄金三步法:看控制台、抓包、比对响应头
-
看控制台
复制报错全文,先判断是 预检失败 还是 真实请求失败。
预检失败:OPTIONS 4xx/5xx;真实请求失败:200 但缺 CORS 头。 -
抓包
打开 Network,重点看 OPTIONS 和真正的请求:- 有没有
Access-Control-Allow-Origin? - 值是否和
Origin一致? - 是否返回了
Allow-Credentials、Allow-Methods、Allow-Headers?
- 有没有
-
比对响应头
用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 安全陷阱:小心暴露敏感头信息或开放过多权限
-
反射 Origin 不带白名单
攻击者用evil.com发请求,后端直接把Allow-Origin回写成evil.com,相当于任意域放行。
解决:永远用白名单数组includes,别偷懒。 -
滥放
Allow-Headers
把Authorization、X-Sign都放过去,结果前端被 XSS 后,黑客可直接拿到Authorization头。
解决:只暴露业务必须的头,敏感操作再额外鉴权。 -
Max-Age过长
预检缓存 86400 秒,后端想紧急关闭某域名,结果用户浏览器一周内仍认为“允许”。
解决:线上默认缓存 1 小时以内,紧急时下线域名立刻改配置。
真实项目中如何设计前后端协作流程避免 CORS 反复出问题
-
项目初始化就定“源”
开发、测试、预发、生产四环境的域名写进 README,后端直接配进代码。
用基础设施即代码(Terraform、K8s ConfigMap)统一管理,禁止上线前临时手动加域名。 -
Mock 优先
前端用 MSW、YApi、Apifox 先跑 Mock,等后端 Ready 再切代理,把 CORS 问题推迟到联调阶段,避免阻塞写页面。 -
接口网关统一收口
把 CORS 配在网关层(Nginx、Kong、APISIX),业务服务无感,想加域名只改网关,降低沟通成本。 -
CI 门禁
写一条自动化测试:用cy.request直接调预检,断言 204,防止后端手滑把 OPTIONS 干成 404。
你以为 CORS 解决了就万事大吉?这些边缘情况照样让你掉头发
-
302 重定向
预检请求 302 跳走,浏览器直接终止后续真实请求。
解决:让后端把 OPTIONS 原地返回 204,别重定向。 -
非简单端口
比如 Electron 里用file://协议打开页面,再调http://localhost也算跨域,且没有 Origin 头。
解决:Electron 关webSecurity或用 IPC 通信。 -
Service Worker 拦截
SW 里fetch(event.request)如果改掉了Origin,浏览器会再检查一次 CORS,导致无限预检。
解决:SW 里克隆请求时别动mode: 'cors'相关字段。 -
混合内容(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等工具 |
吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤
非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!

更多推荐


所有评论(0)