
前端开发者必读:彻底搞懂CORS跨域问题与实战解决方案
- 前端开发者必读:彻底搞懂CORS跨域问题与实战解决方案
- 引言:为什么你的请求总被浏览器“拒之门外”?
- 揭开 CORS 的神秘面纱:从同源策略说起
- CORS 到底是什么?用生活场景讲清楚跨域资源共享机制
- 浏览器眼中的 CORS:预检请求、简单请求与实际请求的区别
- 后端配合有多关键?常见 CORS 响应头字段详解
- 前端发起跨域请求的正确姿势:fetch 与 axios 中的注意事项
- 开发环境下的 CORS 临时绕过技巧
- 生产环境中如何优雅处理 CORS?
- 踩坑实录:那些年我们遇到的 CORS 报错及背后真相
- 当 Access-Control-Allow-Origin 遇到通配符和凭证冲突怎么办?
- 动态设置 CORS 头的高级玩法:按域名、用户或路径灵活授权
- 前端也能主动“防御”:错误捕获、降级方案与用户体验优化
- 调试 CORS 问题的实用工具箱
- 别再盲目加 header 了!理解 OPTIONS 请求的触发条件与性能影响
- 真实项目案例复盘
- 写给未来自己的备忘:一份可复用的 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)。”
登机牌可以是:
- 精准登机牌:
Access-Control-Allow-Origin: https://app.xxx.com - wildcard 登机牌:
Access-Control-Allow-Origin: * - 动态登机牌:后端先瞅一眼你的身份证(Origin 头),再决定给不给你盖章。
一旦盖章,安检就放行;不盖章,直接遣返,还会贴心地附赠一条报错信息,生怕你不知道被谁拒了。
看到这里,聪明的你一定悟了:CORS 的本质是“服务器告诉浏览器,我愿意被谁跨域”。
浏览器只负责检票,不办证,办证得找后端——这也是 90% 的跨域问题最后都变成“后端大哥,加个头”的原因。
浏览器眼中的 CORS:预检请求、简单请求与实际请求的区别
浏览器内部有两条通道:
绿色通道(简单请求):
- 方法只能是 GET/HEAD/POST
- 手动设置的请求头只能出现
Accept、Accept-Language、Content-Language、Content-Type,且Content-Type值只能是application/x-www-form-urlencoded、multipart/form-data、text/plain - 没有上传任何
ReadableStream、Blob等花哨东西
满足以上全部,浏览器觉得“这哥们人畜无害”,直接发请求,再在响应头里检查登机牌。
红色通道(非简单请求):
比如你想带 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-Control、Content-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 });
踩坑点:
- 一旦开启
withCredentials=true,后端必须返回具体源,不能是*,否则浏览器直接拒收。 - 自定义头大小写不敏感,但写错一个字母,预检就过不了。
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?
-
网关层统一收口
把 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 头,前端捕获错误时可能拿不到体,造成“黑盒”。 -
按环境隔离
生产域名严格白名单,测试环境可以用*.test.xxx.com通配,减少摩擦。 -
灰度发布
新增域名先上灰度,确认通行证生效再全量,防止“上线即 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 头的高级玩法:按域名、用户或路径灵活授权
-
按路径
/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; }); } -
按租户
租户 A 自定义域名https://a.com,租户 B 用https://b.com,数据存数据库,启动时刷到内存,请求来时动态匹配,防止租户越权。 -
按用户
高级场景:内网管理员走https://admin.xxx.com,普通客户走https://app.xxx.com,根据 JWT 里的角色再二次校验,前端无感,后端加一层拦截器即可。
前端也能主动“防御”:错误捕获、降级方案与用户体验优化
-
全局捕获
// 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; } } -
自动重试
axios-retry 插件,判断error.code === 'ERR_NETWORK',延迟 2 秒再试一次,降低因 OPTIONS 偶尔 502 造成的白屏。 -
日志上报
因为 CORS 报错拿不到 status,可以把window.onerror里的message、filename、lineno、colno一并丢到 Sentry,辅助后端定位。 -
降级方案
关键接口走同域网关转发,非关键接口(如埋点)允许失败,防止“一颗老鼠屎坏了一锅粥”。
调试 CORS 问题的实用工具箱
-
Chrome DevTools
Network 面板 → 选中请求 → Response Headers → 看有没有 ACAO;如果没有,立即切换到 “Issues” 标签,Chrome 会贴心提示哪一步出错。 -
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 直接锤后端。
-
在线检测
https://www.test-cors.org 一键生成代码,还能可视化查看头,甩给测试妹妹也能看懂。 -
抓包神器 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 问题排查清单
-
看报错关键词:
preflight→ 抓 OPTIONSwildcard *→ 检查是否带 Cookienot allowed by Access-Control-Allow-Headers→ 自定义头未放行
-
快速验证:curl 模拟 OPTIONS,不带业务逻辑,纯粹检查头。
-
确认链路:
浏览器 → 网关 → 业务服务 → 网关 → 浏览器,每一步都可能被吞头。 -
缓存作祟:
后端改完头,记得清 CDN、清浏览器缓存,否则依旧 403。 -
上线 checklist:
- 生产域名字段大小写
- HTTPS 证书有效期
Access-Control-Max-Age是否过大(调试时建议先设 0)- Sentry 是否加了网络错误采样
-
最后一条:永远不要把
*和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等工具 |
吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤
非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!

1074

被折叠的 条评论
为什么被折叠?



