从同源策略到 CORS 的真相:你的请求没被拦住,后端正面临“裸奔”风险

39 阅读7分钟

在 Web 开发中,CORS(跨域资源共享)报错是每一位全栈/前端开发者的“老朋友”。每当我们看到控制台那行红色的 Access to XMLHttpRequest... has been blocked by CORS policy 时,第一反应往往是:“请求跨域被拦截了,后端没收到。”

但这其实是一个巨大的错觉。

要理解这个错觉有多危险,我们必须先让时光倒流,看看浏览器安全规则是如何一步步演进的,你就会明白:CORS 根本就不是为了保护你的后端而诞生的。


一、 黑暗时代:如果没有同源策略会怎样?

想象一下早期的互联网,如果浏览器没有任何跨域限制(即没有同源策略),会发生什么可怕的事情?

假设你刚刚登录了你的网银 bank.com,浏览器保存了你的登录状态(Cookie)。此时,你不小心点开了一个恶意网站 evil.com。 在没有任何限制的“黑暗时代”,evil.com 里的 JavaScript 代码可以为所欲为:

// 恶意网站直接向网银发送请求
fetch('https://bank.com/api/get_balance')
  .then(res => res.json())
  .then(data => {
      console.log("获取到用户的存款余额:", data.balance);
      // 然后把余额数据发送给黑客的服务器
  });

因为浏览器会自动带上你在 bank.com 的 Cookie,网银服务器会认为是你本人在操作,乖乖交出数据。在这个时代,用户的隐私在互联网上相当于“裸奔”。


二、 浏览器的“铁幕”:同源策略(SOP)的诞生

为了阻止这种肆无忌惮的数据窃取,浏览器厂商们联合制定了 Web 安全的基石:同源策略(Same-Origin Policy, SOP)

同源策略规定:只有当“协议、域名、端口”完全相同时,两个网页/请求才能互相读取数据。

有了这道“铁幕”,当 evil.com 试图用 AJAX 读取 bank.com 的数据时,浏览器会直接掐断数据的返回路线。黑客再也拿不到你的网银余额了。


三、 铁幕下的阵痛与 CORS 的引入

同源策略虽然极大地提升了安全性,但它太死板了。 随着 Web 技术的发展,“前后端分离”、“微服务”、“CDN 资源加载”以及“调用第三方 API”成为了常态。

  • 你的前端部署在 www.my-app.com
  • 你的后端 API 部署在 api.my-app.com

因为域名不同,它们触发了同源策略!前端无法读取自己后端的响应数据,正常的业务根本无法开展。

为了在“铁幕”上开一个安全的口子,W3C 引入了 CORS(跨域资源共享) 机制。 CORS 本质上是一套“签证系统”:它允许后端服务器在响应头里加上 Access-Control-Allow-Origin: www.my-app.com,告诉浏览器:“这个域名是我的好兄弟,请允许它读取我的数据。”


四、 残酷真相:CORS 到底拦截了什么?

讲到这里,终于回到了我们开头的问题。既然 CORS 是用来“开绿灯”的,那当我们遇到 CORS 红字报错时,究竟发生了什么?

我们需要纠正一个核心观念:CORS 并不是一道绝对的防火墙。 根据请求类型的不同,浏览器的拦截行为分为两种完全不同的模式:“先斩后奏”“先奏后斩”

1. 简单请求:先斩后奏(最危险的误区)

当你的请求满足“简单请求”条件(通常是 GET 请求,或 Content-Type 为 application/x-www-form-urlencoded 的 POST 请求)时,浏览器的处理流程如下:

  1. 发送: 浏览器直接把请求发给后端(带上 Cookie)。
  2. 执行: 后端收到请求,执行业务逻辑(比如修改数据库、转账、删除用户)。
  3. 响应: 后端处理完毕,返回 HTTP 200 和数据。
  4. 拦截: 浏览器接到响应,检查有没有 CORS “签证”(允许跨域的响应头)。
    • 如果没有,浏览器把响应数据扣下销毁,给前端报 CORS 错误。

结论: 即使 CORS 报错,后端的副作用(Side Effect)已经发生了! 就像是你给隔壁班同学写情书,保安让你寄出去了,同学看完了(业务执行了),回信时保安才把信扣下销毁。你以为信没寄到,其实对方已经读完了。

2. 复杂请求:先奏后斩(安全的预检)

当请求包含特殊头部(如 Authorization)或 Content-Type 为 application/json 时,浏览器会启动预检机制(Preflight)

  1. 探路: 浏览器自动发送一个 OPTIONS 请求,询问服务器:“我能发这个请求吗?”
  2. 审核: 服务器检查 CORS 配置。如果拒绝,浏览器报错,真正的业务请求根本不会发出

结论: 只有在复杂请求下,后端才是安全的。


五、 安全隐患:CORS 挡不住的 CSRF 攻击

既然“简单请求”会直接穿透到后端执行,那么问题来了:如果后端完全依赖 CORS 来做安全防护,会发生什么?

答案是:CSRF(跨站请求伪造)攻击。

假设攻击者在 evil.com 隐藏了这样一段代码:

<!-- 这是一个简单 POST 请求,浏览器会直接发送 -->
<form action="https://bank.com/api/transfer" method="POST">
    <input type="hidden" name="to" value="HackerAccount" />
    <input type="hidden" name="amount" value="10000" />
</form>
<script>document.forms[0].submit();</script>

发生了什么?

  1. 这是一个 Form 表单提交,属于简单请求
  2. 浏览器带上你在 bank.com 的 Cookie,将请求直达后端。
  3. 后端验证 Cookie 有效,执行转账操作
  4. 后端返回“转账成功”。浏览器发现跨域,拦截响应内容并报 CORS 错误。

攻击者的心态: “哈哈!虽然浏览器不让我看‘转账成功’的 JSON 数据,但我根本不在乎响应内容,只要钱转出去就行了!

核心总结:

  • 同源策略/CORS 的本质是保护“读”:它防止恶意网站窃取你的敏感数据。
  • CSRF 的本质是利用“写”:它不需要读你的数据,它只利用你的身份去修改数据。
  • CORS 防不住简单请求的写操作!

六、 后端自保指南:如何封堵漏洞?

既然 CORS 只是防君子不防小人,后端必须构建自己的防线来抵御跨站伪造请求。

1. 严格遵守 HTTP 语义

GET 请求必须是只读的。 千万不要设计 GET /deleteUser?id=1 这种接口。因为攻击者只需要在页面放一个 <img src="..."> 就能悄无声息地触发 GET 请求,连跨域报错都不会有。所有修改数据的操作,必须使用 POST、PUT 或 DELETE。

2. 使用 CSRF Token(经典防线)

  • 原理: 后端生成一个随机的 Token 给前端,前端在提交请求时(Body 或 Header)必须带上这个 Token。
  • 效果: 攻击者在 evil.com 发起伪造请求时,虽然能自动带上 Cookie,但他无法读取网银页面里的 Token(被同源策略挡住了!)。没有 Token,后端直接拒绝执行请求。

3. 配置 SameSite Cookie(现代防线)

在后端设置 Cookie 时,添加 SameSite 属性,直接从根源上切断攻击者的幻想:

Set-Cookie: session_id=xyz; SameSite=Lax;
  • Strict: 只有当前页面和 API 域名完全一致时,浏览器才发送 Cookie。
  • Lax(现代浏览器默认): 允许部分导航行为发送 Cookie,但跨域的 POST/AJAX 等写操作请求绝对不携带 Cookie
  • 效果: 攻击者发起转账请求时,浏览器直接把 Cookie 扣下。后端一看没登录状态,立马打回。

七、 终极总结

Web 安全是一场漫长的猫鼠游戏:

  1. 没有限制导致数据泄露,于是有了 同源策略(SOP)
  2. 同源策略太严影响开发,于是有了 CORS(跨域资源共享)
  3. 同源策略和 CORS 只防“读”不防“写”,于是催生了 CSRF 攻击
  4. 为了防御 CSRF,我们引入了 Token 和 SameSite 机制

做 Web 开发,既要懂“怎么通”(配置 CORS 解决前端报错),更要懂“怎么堵”(保护后端不被恶意调用)。下次再看到 CORS 报错的红字时,在配置 Allow-Origin 之前,不妨先想一想:你的后端,现在“裸奔”了吗?