前后端跨域 CORS

前后端跨域 CORS

用一句话抓住:

跨域是浏览器同源策略拦的,不是服务端收不到请求。

要么服务端发 CORS 头让浏览器放行,要么用代理 / 同域部署让浏览器「看起来不跨域」。


1. 出现原理

1.1 同源策略

浏览器有同源策略(Same-Origin Policy):页面默认只能读写「同源」资源。

同源 = 协议 + 域名 + 端口 三者都相同。任一不同就是跨域。

页面 接口 是否跨域 原因
http://localhost:5173 http://localhost:8080 端口不同
https://a.com http://a.com 协议不同
https://web.a.com https://api.a.com 子域名不同
https://a.com https://a.com/api 三者相同

1.2 为何前后端联调常踩坑

本地常见拆分:

  • 前端:Vite / Webpack 在 http://localhost:5173
  • 后端:Gin / Spring 在 http://localhost:8080

浏览器判定为跨域,于是拦截前端 JS 读取响应。

1.3 核心句细讲:拦的是「读响应」,不是「发请求」

这句话要拆成两层:

  1. 网络层:浏览器往往已经把 HTTP 请求发出去了,服务端也可能已经查库、写库、返回了 200。
  2. 安全层 :响应回到浏览器后,浏览器先按 CORS 规则检查;不通过 ,就不把响应体交给页面里的 JS

所以:服务端日志里可能看到这次请求;前端 await fetch(...) 却进了 catch,控制台报 CORS。两边并不矛盾。

1.3.1 参与者各管什么
角色 做什么 管不管同源
页面 JS 调用 fetch / axios,期望拿到 JSON 不管;它只发起调用
浏览器 真正发包、收包,并执行同源 / CORS 检查 ;不通过就不给 JS 数据
服务端 收请求、跑业务、写响应 不管同源;HTTP 请求照常处理
Postman / curl 自己发 HTTP、自己读响应 不管;所以看起来「接口是好的」

同源策略是浏览器对「网页里的脚本」加的门锁,不是对整个互联网加的门锁。

1.3.2 简单请求的完整时序(服务端通常已收到)

以页面 http://localhost:5173 去请求 http://localhost:8080/api/user 为例(端口不同 → 跨域):

text 复制代码
① 页面 JS
   fetch('http://localhost:8080/api/user')

② 浏览器(发出前)
   · 发现页面 Origin 与目标 URL 不同源 → 这是跨域 XHR/fetch
   · 仍然发出请求(简单请求不会先拦发送)
   · 自动带上:Origin: http://localhost:5173

③ 服务端(8080)
   · 正常收到 GET /api/user
   · 正常查库、组装 JSON
   · 返回 HTTP 200 + body(若未配 CORS,响应里没有
     Access-Control-Allow-Origin)

④ 浏览器(收到后,关键一步)
   · 先看响应头有没有允许当前 Origin 的 CORS 头
   · 没有 / 不匹配 → 判定「页面脚本不能读这份响应」
   · 把错误抛给 JS(CORS policy ...)
   · JS 拿不到 body,哪怕服务端其实返回了 200

⑤ 页面 JS
   · Promise reject / 进 catch
   · 开发者容易误判成「接口挂了」

画成对照:

text 复制代码
                    发出请求                收到响应
页面 JS  ──────►  浏览器  ──────►  服务端  ──────►  浏览器  ──╳──►  页面 JS
                  (放行发送)     (照常处理)      (CORS 检查失败,
                                                    不把 body 交给 JS)

被拦的是箭头最后一截「浏览器 → 页面 JS」,不是中间「浏览器 → 服务端」。

1.3.3 用 Network 面板怎么验证这句话

打开 DevTools → Network,点开那条失败的请求,常能看到:

你看到的 含义
请求确实出现在列表里 浏览器发过包
Status 可能是 200 服务端处理成功并回了包
Response / Preview 可能空白或提示被 CORS 挡住 浏览器不让你(以及页面 JS)读 body
Console 有 CORS 红字 安全策略在「读」这一步否决了

再对比:同一 URL 用 Postman 调用------没有「页面 Origin」,也没有「把结果交给敌对页面脚本」的风险模型,所以直接看到 200 和 JSON。这正好证明:问题不在服务端收不到,而在浏览器不给网页脚本看。

1.3.4 例外:预检失败时,正式请求可能根本没发出

复杂请求(如 Content-Type: application/json、自定义 AuthorizationPUT / DELETE 等)流程不同:

text 复制代码
① JS 要发「复杂」跨域请求
② 浏览器先发 OPTIONS 预检(仍会到服务端)
③ 浏览器检查预检响应的 CORS 头
   · 通过 → 再发真正的 GET/POST/...(服务端会再收到一次)
   · 失败 → 正式请求不再发出;JS 同样报 CORS

因此更精确的说法是:

  • 简单请求 / 预检已通过:服务端通常已处理正式请求;拦的是「JS 读响应」。
  • 预检未通过:正式业务请求可能从未到达服务端;拦在「允不允许发这类跨域请求」。

日常说「不是服务端收不到」,主要针对大家最容易误会的第一种情况------日志里明明有 200,前端却喊跨域。

1.4 简单请求与预检(简表)

类型 条件(示意) 浏览器行为 服务端是否收到正式请求
简单请求 常见 GET / POST + 有限 Header 直接发正式请求,再检查响应头 通常已收到
预检请求 自定义 Header、JSON 体、部分方法等 OPTIONS,通过后再发正式请求 预检失败则正式请求未发

1.5 为何要这么设计(直觉)

若没有这层检查:恶意站点 evil.com 也能让你的浏览器带着你的登录态去读 bank.com 的接口,并把 JSON 交给 evil.com 的脚本。

CORS 的默认态度是:跨域响应默认对网页脚本不可读;只有目标站通过响应头明确说「我允许某某 Origin 读」,浏览器才松手。

服务端照常处理请求,是因为 HTTP 本身没有「同源」概念;安全边界加在浏览器与网页脚本之间。


2. 不解决会出现什么错误

2.1 控制台典型报错

text 复制代码
Access to fetch at 'http://localhost:8080/api/xxx' from origin 'http://localhost:5173'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.

预检失败时常见:

text 复制代码
Response to preflight request doesn't pass access control check

带 Cookie 时还可能:

text 复制代码
The value of the 'Access-Control-Allow-Origin' header in the response must not be
the wildcard '*' when the request's credentials mode is 'include'.

2.2 现象对照

现象 说明
控制台 CORS 红字 浏览器按策略拦截了 JS 读响应
Network 里请求红 / failed 或 OPTIONS 失败导致正式请求未发
fetch / axioscatch 前端拿不到响应体(即便服务端曾返回 200)
Postman 正常、页面异常 典型「只有浏览器拦」的特征

3. 解决方案有多少种

工程上常用 5 类(另有 1 类仅调试、不算产品方案)。

方案 一句话 谁改 典型场景
A. 服务端 CORS 头 告诉浏览器允许该 Origin 后端 前后端不同域且必须直连
B. 开发环境代理 浏览器只打同源,工具转发 前端构建工具 本地联调
C. 反向代理同域 对外同一 Origin 运维 / 网关 生产标准做法
D. BFF / 后端中转 服务端代调真正 API 后端 第三方、聚合、藏真实地址
E. JSONP <script> 绕过 前后端 遗留,不推荐
(非方案)关浏览器安全 仅个人调试 本机浏览器 不能上线

4. 每一种方案通过什么手段解决

4.1 方案 A:服务端开 CORS

手段:在响应里加 CORS 响应头,让浏览器放行「读跨域响应」。

关键响应头:

作用
Access-Control-Allow-Origin 允许的前端源(具体 Origin,或开发时临时 *
Access-Control-Allow-Methods 允许的方法,如 GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers 允许的请求头,如 Content-Type, Authorization
Access-Control-Allow-Credentials 允许带 Cookie 时设为 true(此时 Origin 不能是 *
Access-Control-Max-Age 预检结果缓存秒数

Gin / Express / Spring 等通常用 CORS 中间件统一处理,并正确响应 OPTIONS

本质:不改变「跨域」事实,只让浏览器同意 JS 读响应。

4.2 方案 B:开发环境代理

手段 :前端请求同源路径(如 /api/...),由 Vite / Webpack Dev Server 转发到后端。

浏览器看到的是 5173 → 5173(同源);跨域发生在「开发服务器 ↔ 后端」,浏览器不管。

Vite 示例:

ts 复制代码
server: {
  proxy: {
    '/api': {
      target: 'http://localhost:8080',
      changeOrigin: true,
    },
  },
}

适用:本地联调;生产需另配 Nginx / 网关(见方案 C)。

4.3 方案 C:反向代理 / 同域部署

手段:Nginx / Caddy / API Gateway 把前端与 API 挂在同一域名下:

  • https://app.com/ → 静态前端
  • https://app.com/api/ → 后端服务

浏览器始终同源,自然无 CORS。

本质:从架构上消掉跨域,而不是「允许跨域」。生产最推荐。

4.4 方案 D:BFF / 后端中转

手段:前端只调自己的同源后端(BFF);由 BFF 再请求真正的第三方或内网 API。

跨域发生在「服务端 ↔ 服务端」,不受浏览器同源策略限制。

适用:对接第三方、隐藏真实 API、统一鉴权与聚合。

4.5 方案 E:JSONP(历史方案)

手段 :用 <script src="...?callback=fn"> 加载;服务端返回 fn(data)

限制与风险:

  • 只适合 GET
  • 存在脚本注入等安全风险
  • 现代项目用 CORS / 代理即可,不必再用

4.6 不算正经方案

  • 浏览器关安全策略(如 --disable-web-security):仅个人调试,不能当产品方案。
  • 生产环境对所有源返回 Access-Control-Allow-Origin: *:开发图省事可以,上线应收紧 Origin;带 Cookie 时更不能用 *

5. 怎么选

场景 优先选
本地前后端不同端口 B 开发代理,或 A 后端 CORS 中间件
生产前后端分离域名 C 同域反代优先;必须跨域直连再用 A
调第三方且不想暴露密钥 / 地址 D BFF
老系统只支持 JSONP E 仅维持遗留,新功能勿扩

6. 自检清单

  • 确认是否真跨域:协议 / 域名 / 端口是否有一处不同。
  • 看 Network:是正式请求被拦,还是 OPTIONS 预检失败。
  • 需要 Cookie 时:前端 credentials、后端 Allow-Credentials、具体 Origin(非 *)是否齐全。
  • 用 Postman 能通、浏览器不通:优先查 CORS,而不是先怀疑业务逻辑写错。
    `)
相关推荐
JaguarJack6 天前
Fiber 与 PHP 8.6 Polling API 异步初探
后端·php·服务端
CappuccinoRose10 天前
前端存储体系学习总结文档
前端·客户端·cookie·ssr·服务端·localstorage·sessionstorage
JaguarJack13 天前
对标 npx 的 CPX PHP 的 Composer 包执行器
后端·php·服务端
JaguarJack14 天前
PHP 引用计数机制深度解析
后端·php·服务端
@PHARAOH2 个月前
WHAT - NextAuth 权限认证机制
前端·微服务·服务端
@PHARAOH3 个月前
HOW - 构建一个轻量前后端一体服务
前端·微服务·服务端
2401_841495644 个月前
Linux C++ TCP 服务端经典的监听骨架
linux·网络·c++·网络编程·ip·tcp·服务端
superpigwin5 个月前
使用 OpenSpec + 提示词工程 + Harness 思维,构建一站式多 Agent 研发流程
服务端·研发流程
Sheldon一蓑烟雨任平生5 个月前
WebSocket 聊天室项目(结合Socket.IO)
websocket·客户端·服务端·socket.io·聊天室