
2026-08-17 · 阅读 1
关于跨域和同域
跨域与同域
一句话版本:协议、域名、端口,三者只要有一个不一样,就是跨域;三者完全一样,就是同域。
一、地基知识:什么是同域,什么是跨域
判断标准就一个:协议 + 域名 + 端口,三者完全相同 = 同域。
| 对比 | 结果 | 原因 |
|---|---|---|
http://www.a.com/page vs http://www.a.com/api | 同域 ✅ | 路径不同不算,只看协议域名端口 |
http://a.com vs https://a.com | 跨域 ❌ | 协议不同(http / https) |
http://a.com vs http://www.a.com | 跨域 ❌ | 域名不同(带不带 www 是两个域名) |
http://a.com:80 vs http://a.com:8080 | 跨域 ❌ | 端口不同 |
大白话理解:
- 域名 = 小区名字
- 协议 = 进小区的方式(走正门 / 走地库)
- 端口 = 门牌号
三者全对上,才算"同一个地方的人",即同域;有一个对不上,就是"外来人口",即跨域。
一个重要认知(很多人不知道):
跨域限制只存在于浏览器里。服务器之间互相请求(比如后端调第三方 API)、用 Postman、用 curl,统统没有跨域问题。 因为这个限制是浏览器定给自己的一条安全规矩,别的地方不归它管。
二、为什么浏览器要搞这个限制?—— 它在防贼
想象一个场景:
你登录了 www.bank.com,浏览器帮你存好了登录 Cookie
然后你手贱点开了一个邪恶网站 www.evil.com
如果没有跨域限制,evil.com 的网页里可以偷偷写一行代码:
// 趁你不注意,用你的身份去查银行余额
fetch('https://www.bank.com/api/my-balance')
关键在于:浏览器发请求时会自动带上 bank.com 的 Cookie。银行一看,Cookie 是对的,以为是本人,就把余额乖乖返回了 → evil.com 拿到了你的钱。
所以浏览器立了一条规矩,叫同源策略:
从 A 网站加载的 JS,不许去读写 B 网站的响应。
大白话:你家的保姆(A 站的 JS)不能拿着你家钥匙(Cookie)偷偷去邻居家(B 站)搬东西。
这也顺便解释了为什么 Postman 没有跨域问题——Postman 里没有你浏览器里的 Cookie,贼没钥匙,防不防都无所谓。
三、跨域到底是怎么被拦的?—— 一场"对暗号"
很多人以为跨域报错 = 请求没发出去。错。
真相是:请求发出去了,后端也正常返回数据了,但浏览器把数据扣下了,不给你的 JS 用。整个过程像门卫对暗号:
你的页面运行在 http://localhost:5173(前端开发服务器)
① 你的代码:fetch('http://localhost:3000/api/user')
② 浏览器发请求时,自动贴一张"身份证"(你控制不了,它自动加):
Origin: http://localhost:5173 ← 意思是"我来自 5173"
③ 后端返回数据时,可以贴一张"通行证":
Access-Control-Allow-Origin: http://localhost:5173
← 意思是"我允许 5173 的人来访问我"
④ 浏览器开始对暗号:
身份证 = 通行证 ✅ → 数据交给你的 JS,一切正常
通行证没贴 / 对不上 ❌ → 数据到了也扔掉,
控制台报红字(你熟悉的那坨 CORS 报错)
大白话理解:
Origin请求头 = 你递给门卫的身份证Access-Control-Allow-Origin响应头 = 保安亭贴的白名单
你平时看到的跨域报错,本质就是后端没贴白名单,或者白名单上写的不是你。所以解决跨域,说穿了就是想办法让"身份证"和"白名单"对上。
四、OPTIONS 预检请求 —— 先敲门,再进屋
上面那套流程适用于"老实巴交"的 GET 请求。但如果你的请求比较危险——比如 DELETE(删数据)、或者带上了登录 token 的自定义请求头——浏览器会多一道手续:先发一封问路信。
① 你的代码:
fetch(url, { method: 'DELETE', headers: { Authorization: 'token' } })
② 浏览器先不执行,而是自动发一个 OPTIONS 请求(注意:你代码里根本没写它):
"请问:我能用 DELETE 方法、带着 Authorization 头来访问你吗?"
③ 后端回信:
允许的方法:GET, POST, DELETE
允许的头:Authorization
(并且这个许可有效期 1 天,1 天内不用重复问)
④ 浏览器:问了没问题 → 这才把真正的 DELETE 发出去
大白话理解:
- 普通请求 = 进便利店买瓶水,直接进
- 危险请求 = 进别人家搬家具,得先敲门问一句"我能进来搬吗?",主人点头了才能动
实践提示:F12 打开网络面板,看到正式请求前多了一个 OPTIONS 请求,那不是 bug,是浏览器在替你敲门。
五、怎么解决跨域?(三个常用方案)
方案一:后端贴白名单(CORS 配置)—— 生产环境标准答案
让后端在响应里带上通行证:
// Node.js Express 例子
app.use(cors({ origin: 'http://localhost:5173' }));
大白话:保安亭把你的名字写进白名单,门卫一看对上了,放行。
方案二:开发环境用 Vite 代理 —— 找个中间人捎货
前面说了,跨域限制只在浏览器里。那我不让浏览器直接跨域,找个同域的中间人转发:
没有代理(跨域):
浏览器(5173) ───跨域❌──→ 后端(3000)
有代理(同域):
浏览器(5173) ──同域✅──→ Vite(5173) ──服务器之间无跨域✅──→ 后端(3000)
配置写在 vite.config.js:
server: {
proxy: {
'/api': {
target: 'http://localhost:3000', // 真正的后端地址
changeOrigin: true
}
}
}
之后代码里只写相对路径:
fetch('/api/user') // 请求发给 5173 自己(同域!),Vite 再转交给 3000
大白话理解:你不能出小区(浏览器不能跨域),但可以让小区物业(Vite)帮你跑腿,物业出门没有限制(服务器之间不检查暗号)。浏览器全程以为自己在访问 5173,根本没意识到跨域发生了。
这是日常开发最常用的方案。
方案三:上线后前后端同域部署(Nginx)—— 干脆住一个小区
正式上线时,运维用 Nginx 把前后端挂在同一个域名下:
https://www.mysite.com/ → 返回前端页面
https://www.mysite.com/api/xx → 转发给后端服务
同一个域,门卫根本不拦。
大白话理解:既然出小区要查证件,那前端后端干脆搬进同一个小区,从此不需要出门。
所以你会发现:跨域基本上是个"开发期问题",上线用同域部署就天然消失了。
方案四:本地同域、上线跨域 —— 反过来也行(常见架构)
和方案三正好相反的部署方式,很多公司在用:
【开发环境:同域】(浏览器以为没跨域)
浏览器(5173) ──同域✅──→ Vite ──proxy──→ localhost:3000 后端
【生产环境:跨域】(后端必须配 CORS)
浏览器 ──→ www.mysite.com ← 前端静态资源(放 CDN)
└─→ api.mysite.com ← 后端 API(跨域 ❌,靠白名单放行)
注意 www.mysite.com 和 api.mysite.com 域名不同,是跨域的,所以生产环境必须由后端配 CORS 白名单放行前端域名。
为什么公司要这么部署?
- 前端放 CDN,全球加速、抗流量
- 一套
api.xxx.com同时服务 Web、App、小程序(App 原生请求不受同源策略限制) - 前后端独立扩缩容、独立发版
代码怎么同时兼容两种环境:用环境变量切换请求地址——
# .env.development(开发)
VITE_API_BASE=/api
# .env.production(生产)
VITE_API_BASE=https://api.mysite.com
const baseURL = import.meta.env.VITE_API_BASE;
fetch(`${baseURL}/user`);
跨子域共享 Cookie:www 和 api 虽跨域但同属主域 mysite.com,后端把 Cookie 设成 Domain=.mysite.com,两个子域的请求就都能带上这把钥匙,登录态打通。
方案四的坑:本地可以、上线报错?
这个架构确实有风险:本地走代理一路绿灯,上线后白名单写错(比如少了 https、带不带 www 写错),第一个请求就 CORS 报错。
但它依然是行业主流,因为:
- 跨域失败是"响亮失败":上线第一个请求就大红字,立刻发现,改一行配置就能修。不像内存泄漏那种静默 bug 藏三天。
- 正规团队有测试环境兜底:测试环境用和生产一样的域名结构(如
test.xxx.com+api-test.xxx.com),CORS 问题在测试环境就爆了,轮不到生产。 - 换来的架构收益(CDN、多端共用 API、独立扩容)是实打实的。
大白话理解:坑有两种——跳进去会喊疼的(CORS 配置错,秒发现)和底下埋刀的(静默 bug,深夜爆炸)。方案四属于前者,只要测试环境和生产"长得一样",坑在开发期就被踩掉了。真正挖坑的不是这个方案,而是"上了跨域架构却没有测试环境"。
六、一个高频坑:跨域时 Cookie 带不上
默认情况下,跨域请求不携带 Cookie。你要带的话,必须前后端同时配合:
// ① 前端:明确声明要带 Cookie
fetch(url, { credentials: 'include' })
// ② 后端:白名单必须写具体地址,不能偷懒用 *
Access-Control-Allow-Origin: https://www.frontend.com // ✅ 精确指定
Access-Control-Allow-Credentials: true
// ❌ 如果写 Access-Control-Allow-Origin: * 会直接报错,
// 浏览器认为"通配符 + 带凭证"太危险,不允许
大白话理解:默认出门不带钥匙(Cookie),怕丢。你要带钥匙,就得让目的地确认"只认你这一个人",写 *(谁来都行)这种敷衍白名单,门卫反而不敢放你带钥匙进。
七、全文速查表
| 概念 | 一句话解释 |
|---|---|
| 同源策略 | 浏览器防贼:A 站的 JS 不许读 B 站的响应 |
| 同域 | 协议 + 域名 + 端口三者完全相同 |
| 跨域 | 三者有任何一个不同 |
Origin 请求头 | 浏览器自动贴的"身份证"(我来自哪) |
Access-Control-Allow-Origin 响应头 | 后端贴的"白名单"(我允许谁来) |
| OPTIONS 预检请求 | 危险请求前,浏览器先替你问一句"我能来吗" |
| CORS | 后端贴白名单的那套标准机制 |
| Vite 代理 | 开发时找同域中间人转发,让浏览器以为没跨域 |
| Nginx 同域部署 | 上线后前后端一个域名,问题直接消失 |
credentials: 'include' | 跨域时想带 Cookie,前后端都得声明 |
八、面试总结(背这段就够了)
同域是协议、域名、端口三者完全相同;跨域限制是浏览器的同源策略,目的是防止恶意网站借用用户的 Cookie 偷取其他网站的数据,所以它只存在于浏览器,服务器之间和 Postman 都不受限。
拦截的机制是:浏览器自动带
Origin头,后端通过Access-Control-Allow-Origin响应头声明白名单,对不上浏览器就扣下响应。对于 DELETE、PUT 或带自定义头的"危险请求",浏览器会先发 OPTIONS 预检,拿到许可后才发真正请求。解决方案:生产环境由后端配置 CORS 白名单,或用 Nginx 把前后端部署在同一域名下;开发环境用 Vite 的 proxy 代理,让请求先发给同域的开发服务器再转发,从根本上绕开浏览器限制。
写在最后
回到最初那句话:
协议、域名、端口不一样就是跨域,相同就是同域。
这句话是地基,上面所有的内容其实只是在回答三个问题:
- 浏览器为什么拦? —— 防贼(防止恶意网站借你的 Cookie 偷数据)
- 怎么拦的? —— 对暗号(Origin 身份证 vs Allow-Origin 白名单)
- 怎么过? —— 让暗号对上(后端贴白名单),或者让浏览器根本不知道跨域了(代理 / 同域部署)