前端看后端 14:什么是 CORS?

专栏第十四篇,网络与协议篇第七章。上一篇我们聊了 Cookie 和 Session,文末预告了今天的主角。做前后端分离开发时,你大概率见过这样的画面:前端跑在 localhost:3000,后端跑在 localhost:8080,接口明明写对了,浏览器却一片红------No 'Access-Control-Allow-Origin' header is present。这就是每个前端工程师都绕不开的"拦路虎"------CORS(跨域资源共享)。今天我们就把它彻底讲明白:跨域到底是怎么回事?预检请求为什么存在?带 Cookie 时又有哪些坑?


一、先看症状:那片熟悉的红色报错

只要你做过前后端分离开发,一定见过它:

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

后端说:"我接口明明返回 200 了,数据也查出来了,你前端是不是写错了?" 前端说:"Network 里明明能看到响应,可 JS 就是拿不到数据!"

问题不在后端,也不完全在前端------问题出在浏览器这个"中间人"身上 。响应其实已经回来了,但浏览器看了一眼响应头,发现"没有通行证",于是把数据扣押了,不让 JS 读取。

这就是跨域。


二、什么是"域"(Origin)?

要理解跨域,先得知道什么叫"同源"。

域(Origin)= 协议 + 域名 + 端口。三者必须完全一致,才叫同源。只要有一个不一样,就是跨域。

URL A URL B 是否同源 原因
http://localhost:3000 http://localhost:4000 ❌ 跨域 端口不同
http://127.0.0.1:3000 http://localhost:3000 ❌ 跨域 域名不同
https://example.com http://example.com ❌ 跨域 协议不同
https://api.example.com https://www.example.com ❌ 跨域 二级域名不同
https://www.example.com/a https://www.example.com/b ✅ 同源 路径不影响

注意最后一行:路径不一样不算跨域 。很多初学者会以为 /a/b 是不同源,其实它们同源。跨域比的是"站",不是"页面"。


三、浏览器为什么要拦你?(同源策略 SOP)

很多人第一次遇到跨域时,都会在心里骂一句:"浏览器是不是有病?我自己调我自己的接口都不行?"

但这真不是浏览器多管闲事,它是在保护你 。这背后是浏览器的安全基石------同源策略(Same-Origin Policy, SOP)

假设没有跨域限制......

  1. 你登录了 bank.com,Cookie 里存了你的登录态。
  2. 你又打开了一个恶意网站 evil.com
  3. evil.com 页面里的 JS 偷偷向 bank.com/api/transfer 发了一个转账请求。
  4. 浏览器会自动带上 bank.com 的 Cookie(上一篇讲过,Cookie 是自动携带的)。
  5. 服务器一看 Cookie 是合法的,转账成功。

这就是著名的 CSRF(跨站请求伪造) 攻击。

为了防止这种情况,浏览器规定:脚本只能自由访问同源的资源。不同源的请求,要经过一套授权机制------这套机制就是 CORS。

💡 同源策略限制的是脚本读取响应,它并不阻止请求发出去。这也是为什么你在 Network 面板里能看到请求和响应,但 JS 拿不到。理解这一点非常关键。


四、什么是 CORS?(官方解法)

CORS = Cross-Origin Resource Sharing(跨域资源共享) ,是 W3C 的标准,也是解决跨域最正统、最官方的方案。

它的核心思想就一句话:

浏览器继续拦,但让服务器来决定"是否放行"。

服务器通过在 HTTP 响应头里加上几个特定字段,告诉浏览器:"这个请求我允许来自 http://localhost:3000 的访问,你别拦。"浏览器看到通行证,就把扣押的数据交给 JS。

CORS 不是要你绕过浏览器的安全机制,而是让服务器主动、显式地授权


五、CORS 是怎么工作的?(两种请求)

CORS 不是简单加个 Header 就完事。浏览器根据请求的"危险程度",把它分成两类,处理方式完全不同。

1. 简单请求(Simple Request)

同时满足以下条件,才叫简单请求:

  • 方法只能是 GETPOSTHEAD
  • 只能使用这几个安全头:AcceptAccept-LanguageContent-LanguageContent-Type
  • Content-Type 的值只能是 application/x-www-form-urlencodedmultipart/form-datatext/plain

也就是说,我们常用的 Content-Type: application/json 的 POST 请求,不属于简单请求。

流程:

text 复制代码
浏览器                                      服务器
  |  Origin: http://localhost:3000          |
  |---------------------------------------->| (直接发请求)
  |                                         |
  |  Access-Control-Allow-Origin: *         |
  |<----------------------------------------| (服务器回应是否允许)
  |                                         |

浏览器在请求头里带上 Origin 字段,表明"我从哪来";服务器在响应头里返回 Access-Control-Allow-Origin,表明"我允许谁来"。两边对得上,就放行。

2. 预检请求(Preflight Request)

大部分 AJAX 请求都属于这一类 ,比如 Content-Type: application/json、自定义请求头(Authorization)、PUT/DELETE 方法等。

浏览器不放心,会在真正发请求之前,先问一嘴------这就是"预检"。

完整流程分三步:

第一步:浏览器先发一个 OPTIONS 请求。

http 复制代码
OPTIONS /api/data HTTP/1.1
Host: localhost:8080
Origin: http://localhost:3000
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type

浏览器在问:"我打算用 POST 方法、带 Content-Type 头发请求,你允许吗?"

第二步:服务器回应。

http 复制代码
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

服务器说:"可以,这几个方法和头我都接受。24 小时内(86400 秒)别再来问了。"

第三步:预检通过,浏览器才发送真正的业务请求。

http 复制代码
POST /api/data HTTP/1.1
Host: localhost:8080
Origin: http://localhost:3000
Content-Type: application/json

{"name": "张三"}

💡 为什么要多此一举搞预检? 因为有些老旧的服务器根本不认识 CORS,也不支持 PUT/DELETE。如果浏览器直接发一个 DELETE 过去,可能会在服务器上造成真实的副作用(删数据)。预检机制保证了:在确认服务器"懂 CORS 且愿意接受"之前,不发送任何可能产生副作用的真实请求。这是一种对老系统的保护。


六、CORS 的核心响应头(重点)

这些响应头是服务器必须配置的,也是面试常考的点:

响应头 作用
Access-Control-Allow-Origin 必填 。允许的源,值为 *(允许任意)或具体域名(如 http://localhost:3000)。
Access-Control-Allow-Methods 预检响应。允许的 HTTP 方法,如 GET, POST, PUT, DELETE
Access-Control-Allow-Headers 预检响应。允许的请求头,如 Content-Type, Authorization
Access-Control-Allow-Credentials 重要 。设为 true 才允许跨域请求携带 Cookie。
Access-Control-Max-Age 预检结果缓存时间(秒),期间不再重复发 OPTIONS。
Access-Control-Expose-Headers 允许 JS 读取的响应头白名单。默认只能读 Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma 这几个。

Expose-Headers 这个头容易被忽略。如果你在响应里放了一个自定义头(比如 X-Request-Id),前端在 response.headers 里是读不到的,除非服务器把它列进 Access-Control-Expose-Headers


七、CORS 与 Cookie:一个经典大坑

上一篇我们讲了 Cookie/Session 登录。跨域时带 Cookie,有一个非常容易踩的坑。

前端要做的

默认情况下,跨域请求不会携带 Cookie。你需要显式打开:

javascript 复制代码
// axios
axios.defaults.withCredentials = true;

// fetch
fetch(url, { credentials: 'include' });

后端必须做的

这才是关键,两个响应头缺一不可:

http 复制代码
Access-Control-Allow-Origin: http://localhost:3000   // ❗不能是 *
Access-Control-Allow-Credentials: true

注意:当 Allow-Credentialstrue 时,Allow-Origin 绝对不能是 *,必须是明确的域名。否则浏览器会直接拒绝。

为什么不能用通配符?

这是浏览器的安全规定。如果 Allow-Origin: * 同时又允许带凭证,浏览器无法判断该把哪个域的 Cookie 发过去,等于把所有网站都放进了信任名单------这就违背了同源策略的初衷。所以浏览器要求:要带 Cookie,就必须精确指定来源

💡 同理,Access-Control-Allow-HeadersAccess-Control-Allow-Methods 在带凭证时也不能用 *(虽然这一条各浏览器实现略有差异,但最好都显式列出)。


八、CORS 的常见应用场景

1. 前后端分离开发(最常见)

  • 场景 :前端 localhost:3000(React/Vue),后端 localhost:8080(Spring Boot/Express)。
  • 做法 :后端配置 Access-Control-Allow-Origin: http://localhost:3000

2. 调用第三方 API

  • 场景:前端直接调用高德地图、天气等公开 API。
  • 做法:第三方服务器必须开启 CORS,否则你只能让自己的后端做代理转发。

3. 微服务网关

  • 场景www.example.com 调用 api-user.example.comapi-order.example.com
  • 做法:在网关层(Nginx/Kong/Spring Cloud Gateway)统一配置 CORS,避免每个服务各写一遍。

4. WebSocket 的"跨域"

WebSocket 不受同源策略限制 ,浏览器允许从任意页面发起 WS 连接。但 WS 有自己的握手校验机制,而且如果握手时依赖 Cookie/Session,浏览器仍会遵循 Cookie 的 SameSite 规则。


九、解决跨域的其他方案(对比)

除了 CORS,开发中还有几种常见手段:

方案 原理 适用场景 缺点
CORS 服务器返回授权头 生产环境首选 需要服务器配置支持
Nginx 反向代理 把跨域请求伪装成同源 生产环境、运维统一配置 需要服务器权限
Node 代理(Vite/Webpack) 开发服务器中间件转发 本地开发首选 只在开发环境生效
JSONP 利用 <script> 标签不受同源限制 老式浏览器、只支持 GET 仅 GET、有安全隐患、已淘汰

开发环境最佳实践:代理

本地开发时,最省事的做法不是让后端配 CORS,而是用前端构建工具的代理:

javascript 复制代码
// vite.config.js
export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },
});

这样前端访问 /api/users,实际上是 Vite 开发服务器帮你转发到 http://localhost:8080/api/users。浏览器看到的是同源请求,自然不拦截。请求是在服务器端转发的,不经过浏览器的同源策略。

上线之后怎么办?两种选择:

  1. 后端直接配 CORS,灵活可控;
  2. 用 Nginx 把前端静态资源和 API 代理到同一个域名下,从根本上消除跨域。

十、串联:一个完整的跨域登录流程

把前两篇的内容串起来,看一个真实的登录流程:

  1. 跨域请求 :浏览器(localhost:3000)向 API(localhost:8080)发登录请求,Content-Type: application/json
  2. 预检介入 :浏览器发现是非简单请求,先发一个 OPTIONS 预检;服务器返回允许的方法和头。
  3. 正式请求:预检通过,浏览器发送真正的 POST 请求,带上用户名密码。
  4. Set-Cookie :服务器校验密码,创建 Session,通过 Set-Cookie: sessionId=xxx; HttpOnly; SameSite=Lax 下发。
  5. Cookie 入库 :浏览器收到 Cookie,校验 DomainSameSite 等属性后存储。
  6. 后续请求 :前端再次发起跨域请求,因为配置了 withCredentials: true,浏览器自动带上 Cookie。
  7. CORS 校验 :服务器返回 Access-Control-Allow-Origin(具体域名)和 Access-Control-Allow-Credentials: true,浏览器放行。
  8. 鉴权通过 :服务器通过 Cookie 里的 sessionId 查到 Session,确认用户身份,返回数据。

看,CORS 和 Cookie/Session 就是这样配合工作的。


十一、总结

  • 同源策略:浏览器的安全锁,协议+域名+端口三者一致才叫同源,主要用来防 CSRF。
  • CORS:服务器返回的一张"通行证",显式授权哪些跨域请求可以被接受。
  • 简单请求 vs 预检请求:简单请求直接发;非简单请求先发 OPTIONS 预检,再发真实请求。
  • 带 Cookie 的坑 :前端开 withCredentials,后端 Allow-Credentials: trueAllow-Origin 不能用 *
  • 实践经验:本地开发用 Vite/Webpack 代理,生产用 CORS 或 Nginx 反向代理;JSONP 已经是历史。

写在最后

CORS 本质上不是什么高深的技术,它更像是浏览器和服务器之间的一套"礼貌用语"------浏览器问一句"可以吗",服务器答一句"可以"。但很多人一直停留在"复制一段配置解决报错"的阶段,没真正理解预检、凭证、响应头之间的关系。

理解了 CORS,你就能想明白:

  • 为什么 POST 一个 JSON 会发两个请求(OPTIONS + POST);
  • 为什么后端说"我 Postman 调得通",前端却调不通;
  • 为什么带 Cookie 时把 Allow-Origin 设成 * 反而报错;
  • 为什么 Vite 的 proxy 配一下,跨域就"消失"了。

下一篇,我们来聊一个你每天上网都在默默使用、却很少深究的系统:什么是 DNS?为什么在浏览器里输入一个域名就能找到服务器?域名解析的完整过程是怎样的?A 记录、CNAME、NS 记录又分别是什么? 敬请期待。

如果这个系列对你有帮助,欢迎点赞、关注、收藏三连,我们下篇见 👋

相关推荐
sugar__salt1 小时前
Vue3 自定义指令与插槽(Slot)技术详解
前端·javascript·vue.js·前端框架·vue
Csvn2 小时前
🎯 Flex 布局的 `min-width: auto` 陷阱:为什么内容总是撑破容器?
前端
果然_2 小时前
纯前端图片压缩怎么做?不用上传服务器,浏览器里跑完所有逻辑
前端
Csvn2 小时前
🖼️ OffscreenCanvas:把 Canvas 绘制搬出主线程,动画卡顿的终极解药
前端
用户69371750013842 小时前
了解一下 Agent Harness
android·前端·后端
swipe2 小时前
16|(前端转全栈)前端人排查后端问题:curl、traceId、日志、MySQL、Redis 怎么用?
前端·后端·面试
anyup2 小时前
uni-app 没有根组件?仅需几行代码实现全局 Toast 和 Modal
前端·架构·uni-app
亦暖筑序2 小时前
重新认识 AgentScope-Java 2.0:ReActAgent 负责推理,HarnessAgent 负责运行
java·后端·agent
小林ixn2 小时前
Redis 实战避坑指南:从缓存击穿到高可用,一文全搞定
redis·后端