
前端开发者必看:彻底搞懂CORS跨域问题(原理+实战+避坑指南)
- 前端开发者必看:彻底搞懂CORS跨域问题(原理+实战+避坑指南)
- 引言:你是不是又被那个烦人的“跨域错误”拦住了?
- 跨域到底是个啥?从浏览器安全说起
- CORS机制全解析:浏览器和服务器是怎么“商量”跨域的
- 预检请求(Preflight)到底在干啥?为什么有时候发两次请求?
- 前端常见的跨域场景大盘点:开发、联调、上线全阶段
- 开发环境下的跨域处理妙招:代理、插件、本地服务怎么选
- 生产环境跨域实战:如何配合后端正确配置Access-Control-Allow-Origin
- 那些年我们踩过的CORS坑:凭据、自定义头、状态码的隐藏雷区
- 遇到跨域报错别慌:一步步排查问题的实用思路
- 让跨域不再头疼的前端技巧:封装请求、统一拦截、错误兜底
- 你以为CORS只是后端的事?前端也能主动“助攻”跨域配置
- 彩蛋:一行代码让 OPTIONS 消失?
- 收个尾
前端开发者必看:彻底搞懂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:443 | https://a.com/api | ✅ | 一模一样 |
https://a.com | http://a.com | ❌ | 协议不同 |
https://a.com | https://b.com | ❌ | 域名不同 |
https://a.com:443 | https://a.com:8080 | ❌ | 端口不同 |
浏览器就像固执的保安:
“证件不符?对不起,枪毙。”
可现实总让人啪啪打脸——前后端分离已成标配,页面和接口常常不在一个域。于是 W3C 老大哥站出来:
“别动不动就枪毙,咱走个流程,协商一下。”
这个“协商流程”就是 CORS(Cross-Origin Resource Sharing,跨域资源共享)。
CORS机制全解析:浏览器和服务器是怎么“商量”跨域的
CORS 本质上是浏览器与服务器的一次暗号对接。流程分两种:
- 简单请求(Simple Request)
- 非简单请求(Preflighted Request)
简单请求:浏览器“放行条”直接过
满足以下所有条件,就是简单请求:
- 方法:GET、HEAD、POST 之一
- 头:仅接受 CORS 安全集合(如
Accept、Content-Type: text/plain等) - 无自定义头
- 请求体只支持三种
Content-Type:text/plain、multipart/form-data、application/x-www-form-urlencoded
此时浏览器只在请求头里悄悄塞一个:
Origin: https://h5.xxx.com
服务器如果愿意,就回一个:
Access-Control-Allow-Origin: https://h5.xxx.com
浏览器一看:“暗号对上了,过!”
非简单请求:先“发一封邮件”再进门
一旦用了 PUT、DELETE,或者自定义头,浏览器就会先发一次 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),安检员记住你,今天之内不再搜身。
代码层面,如何减少预检?
- 尽量用简单请求
- 别把
Content-Type写成application/json——一写就预检 - 让后端把
Access-Control-Max-Age调大,浏览器会缓存预检结果,86400 秒(24h) 不重复安检
前端常见的跨域场景大盘点:开发、联调、上线全阶段
| 阶段 | 典型地址 | 跨域? | 常见症状 |
|---|---|---|---|
| 本地开发 | localhost:3000 → localhost:8080 | ✅ 端口不同 | 控制台一片红 |
| 本地联调 | localhost:3000 → dev.api.com | ✅ 域名不同 | 预检 405 |
| 测试环境 | https://test.xxx.com → https://api.xxx.com | ✅ 子域不同 | 带 cookie 失败 |
| 生产环境 | https://h5.xxx.com → https://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。
前端一脸懵:“到底错在哪?”
解决方案:
- Nginx 加
always - 后端在 2xx 里包自定义 code,别滥用 HTTP 状态码
遇到跨域报错别慌:一步步排查问题的实用思路
-
看控制台
复制第一条红色报错,全文搜索CORS,找到关键词blocked by CORS policy -
确认触发场景
- 开发环境?先检查代理是否生效
- 生产环境?抓包看响应头
-
抓包对比
- 没收到 OPTIONS → 简单请求,重点看
Access-Control-Allow-Origin - OPTIONS 405 → 后端没允许 OPTIONS 方法,Nginx 把 OPTIONS 当静态文件了
- 没收到 OPTIONS → 简单请求,重点看
-
检查响应头
用curl -H "Origin: https://h5.xxx.com" -I https://api.xxx.com/user迅速验证 -
最小复现
新建一个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>
- 逐级定位
代理 → 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/info | GET | X-Token | 是 | https://h5.xxx.com |
| /upload | POST | X-Upload-Id | 否 | https://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等工具 |
吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤
非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!


&spm=1001.2101.3001.5002&articleId=154442124&d=1&t=3&u=4684c6a46d384d42bbc7fe39e0adb81b)
4591

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



