CORS 与跨域:从误解到理解
整理自一次完整的问答过程。核心不是记住 CORS 怎么配,而是分清它属于哪一层问题。
一、一句话结论
CORS 是浏览器的一道"归还响应"的许可,不是身份认证,不是权限控制,也不是部署工具。
它只回答一个问题:这个页面,能不能读我返回的数据? 它不回答:你是谁?你能不能调?你该看到什么?
二、判定规则:什么叫"同源"
浏览器比较两个地址的三个值,全部相同才算同源:
|----|--------------------|----------------------------|
| 组成 | 说明 | 举例 |
| 协议 | http / https 不同即跨域 | http vs https |
| 主机 | 域名或 IP 不同即跨域 | app.com vs api.app.com |
| 端口 | 数字不同即跨域 | :5173 vs :8080 |
关键认知:端口是独占资源。 一个端口同一时刻只能被一个进程监听。所以:
- 两个进程 → 必然两个端口 → 必然不同源
- 想把 Spring Boot 的端口改成 5173 来"凑同源" → 直接报
Port 5173 was already in use
改 server.port 只是在两个不同源之间挪位置,挪不出同源。
三、CORS 在请求链路的哪个位置
三个要点:
- ①和②是两次完全独立的请求,方向、目标都不同
- 页面地址只决定
Origin头的值,从不决定请求发给谁 ;请求发去哪只看fetch()里写的字符串
- 后端从未访问过 5173,它只监听自己的端口
后端到底执行了没有?
|---------------------------------------------|----------|----------------------------------|
| 现象 | 后端执行了吗 | 真实原因 |
| 控制台报 CORS 红字,Network 里请求 200 且响应体完整 | 执行了 | 浏览器把响应扣下,不给 JS 读 |
| OPTIONS 预检返回 200 但报 CORS 错 | 没执行 | 预检没过,真实请求压根没发 |
| ERR_CONNECTION_REFUSED / 超时 | 没执行 | 服务没启动,或端口写错 |
| 200 但 res.json() 报 Unexpected token '<' | 没执行 | 请求打到了 dev server,返回的是 index.html |
| 后端返回 404 且有 JSON 错误体 | 到了,但路由不对 | 路径拼错,比如少个 /api |
记住:CORS 失败不会让后端"不干活" 。简单请求下数据已经写进库了,只是 JS 读不到返回值。 唯一例外是非简单请求(带
Authorization、Content-Type: application/json的 POST/PUT/DELETE)------ 预检不过,真实请求不会发出。
四、六个误解与纠正
误解 1:前后端的 URL 必须不同,所以才需要 CORS
纠正:可以完全相同,生产环境通常就是相同的。
CORS 不要求你分域,它只是"当确实分域时"的放行开关。开发期端口不同,是两个独立进程的结果,不是设计要求。
误解 2:用 CORS 可以区分管理员页面和普通用户页面
纠正:CORS 分辨不了"谁",它连"你是谁"都不知道。
它唯一能看到的是 Origin 请求头,只说明"请求来自哪个页面地址",而且 curl 一行就能伪造,毫无可信度。
真正区分权限靠登录凭证:
核心观念:后端不相信"你从哪个页面来",只相信"你带了什么凭证"。 前端隐藏菜单、路由守卫都只是体验,不是权限------接口必须自己校验一次。
误解 3:地址都不一样了,后端怎么会处理这个请求
纠正:后端只监听自己的地址,不关心你在哪个端口看页面。
就像打电话:你拨 8080,8080 接了正常办事;来电显示上有你的号码(Origin),但办事不依赖你的号码是多少。
类比:Origin 是信封上的寄件地址,不是身份证。
误解 4:前端请求成功了,但后端要的是 8080,所以功能没实现
纠正 :先看 Network 面板里的 Request URL 到底是什么。
- 是
8080/api→ 后端一定在处理,功能是实现的,只是浏览器不给 JS 读响应
- 是
5173/api(相对路径又没配代理)→ 后端根本没收到,这才叫没实现
误解 5:CORS 是把前后端"合并"到一起的工具
纠正:CORS 不搬文件、不转发请求,只回几个响应头。
真正做合并的是构建部署 (npm run build + Nginx/静态托管)或反向代理(真转发请求)。
|---------|-------------|-------|-----------|
| 机制 | 做了什么 | 浏览器看到 | 是否需要 CORS |
| 构建 + 部署 | 真的把文件放到一起 | 同一个源 | 不需要 |
| 反向代理 | 真的转发请求 | 以为同源 | 不需要 |
| CORS | 什么都没搬,只回响应头 | 两个源 | 这就是它 |
所以 CORS 不是"合并工具",而是合并不了时的 B 计划。
误解 6:给第三方提供接口,URL 不能一样,所以要配 CORS
纠正:两处都不对。
- URL 完全可以一样 ,第三方就调
https://app.com/api/open。给开放接口单独开前缀是为了配额、版本、审计,跟跨域无关。
- CORS 对第三方调用通常用不上:
|-----------------|------------------|----------------------------|
| 调用方式 | CORS 是否相关 | 准入靠什么 |
| 第三方服务端调你(占绝大多数) | 完全无关,没有浏览器参与 | API Key / 签名 / IP 白名单 / 限流 |
| 第三方网页 JS 直连(少见) | 相关,需白名单或通配 | 仍然靠 Token,CORS 只是放行 |
CORS 从来不决定"谁有权调用",只决定"浏览器愿不愿意把响应交给那个页面"。
五、三种部署形态与 CORS 处理
|-------|------------------------------------|-------------------------------|
| 场景 | 部署形态 | CORS 怎么处理 |
| 开发联调 | Vite 5173 + 后端 8080 | 二选一:前端 proxy 或后端白名单 |
| 生产·同域 | 静态文件 + Nginx 反代(页面与 /api 同域) | 不用配,天然同源 |
| 生产·分域 | app.com + api.app.com,或前端上 CDN | 后端必须配(回显具体源 + 处理 OPTIONS) |
达成"同源"只有三种方式
- 一个进程同时提供页面和接口(Spring Boot +
static/,最直接)
- 反向代理统一入口(Nginx 对外 443,内部分发给两个进程)
- 前端打包成静态文件交给后端/Nginx 托管(本质是 1 或 2)
反过来说:只要浏览器直接访问两个不同端口,就一定跨域。
为什么会有多个坐标?
业务隔离靠路径,不靠换坐标。 一个域名装下所有业务完全没问题:
坐标真正会被拆开,是被这四件事逼的------它们全都与"分清谁在调用"无关:
- CDN 加速:静态资源要放边缘节点,Spring Boot 跑不到 CDN 上
- 独立发版:前端一天十次,后端一周一次,放一起每次改样式都要重启 JVM
- 安全边界:后端只开在内网,公网只暴露网关的 443
- 开发工具链:Vite 是 Node 进程,Spring 是 JVM 进程,物理上不可能共用一个端口
多坐标是成本,不是收益。 每多一个坐标,就多一套 DNS、证书、CORS 白名单和排查时间。 默认一个坐标,能不拆就不拆。
六、实战:Spring Boot 3 项目
如果你用 Spring Boot 托管页面(static/、Thymeleaf、Swagger)
页面和接口都在 8080,同源,永远用不到 CORS,一行都不用写。
这是最省心的形态,继续用就好。
如果将来接入 Vite / React / Vue
一旦 npm run dev 起了 5173,跨域立刻出现。两种解法:
方案 A:前端代理(开发期最省事,后端零改动)
代码里写 fetch('/api/users') 即可。浏览器看到的是 5173 发给 5173,同源,CORS 不触发。
注意:proxy 是 dev server 的功能,打包上线后不存在。生产环境还要再面对一次。
方案 B:后端白名单(生产环境的正解)
两个必踩的坑
allowedOrigins("*")和allowCredentials(true)不能共存,浏览器直接拒绝。带 Cookie 必须回显具体来源。
- 必须能响应
OPTIONS预检 ,否则带Authorization或 JSON body 的请求全部发不出去。
为什么"通常都在后端配置"是对的
因为 CORS 只能配在后端 ------它是响应头,前端写什么都改变不了浏览器的判定。 前端那个配置叫 proxy,作用恰恰相反:让后端不用配 CORS。
开发期二选一,生产期最终都落到后端。
七、排错三步法
- Network 面板 看
Request URL:是 8080 还是 5173,一眼定生死
- 看
Response里有没有真实 JSON:有,说明后端干完活了
- 用
curl打同一个地址:能拿到数据,说明问题只在浏览器这一层
因为 curl 不是浏览器,没有同源策略,也没人拦它。这也再次说明 CORS 只是浏览器给自己定的规矩。
八、决策清单:我需要配 CORS 吗?
九、记忆锚点
- 同源 = 协议 + 主机 + 端口,三者全同
- Origin = 信封上的寄件地址,不是身份证
- CORS = 浏览器归还响应的许可证,不是权限系统
- 鉴权 = 看 Token 里的 role,不看请求从哪来
- 同源方案 = 一个进程 / 反向代理 / 静态托管,三条路
- 改端口 = 只换坐标值,凑不出同源
- 浏览器一直在做同源判定,你感知不到只是因为结果一直是通过
附:本次讨论解决的一个真实困惑
"我用 Spring Boot 3,页面从 8080 打开,测试阶段从来没遇到跨域。"
这不是运气,是部署形态本来就不跨域。同源时浏览器判定为通过,CORS 机制静默存在但不触发。
只有在引入 Vite dev server、把前端放到 CDN、或前后端拆成两个容器时,它才会找上门。