在 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 请求)时,浏览器的处理流程如下:
- 发送: 浏览器直接把请求发给后端(带上 Cookie)。
- 执行: 后端收到请求,执行业务逻辑(比如修改数据库、转账、删除用户)。
- 响应: 后端处理完毕,返回 HTTP 200 和数据。
- 拦截: 浏览器接到响应,检查有没有 CORS “签证”(允许跨域的响应头)。
- 如果没有,浏览器把响应数据扣下销毁,给前端报 CORS 错误。
结论: 即使 CORS 报错,后端的副作用(Side Effect)已经发生了! 就像是你给隔壁班同学写情书,保安让你寄出去了,同学看完了(业务执行了),回信时保安才把信扣下销毁。你以为信没寄到,其实对方已经读完了。
2. 复杂请求:先奏后斩(安全的预检)
当请求包含特殊头部(如 Authorization)或 Content-Type 为 application/json 时,浏览器会启动预检机制(Preflight):
- 探路: 浏览器自动发送一个
OPTIONS请求,询问服务器:“我能发这个请求吗?” - 审核: 服务器检查 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>
发生了什么?
- 这是一个 Form 表单提交,属于简单请求。
- 浏览器带上你在
bank.com的 Cookie,将请求直达后端。 - 后端验证 Cookie 有效,执行转账操作。
- 后端返回“转账成功”。浏览器发现跨域,拦截响应内容并报 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 安全是一场漫长的猫鼠游戏:
- 没有限制导致数据泄露,于是有了 同源策略(SOP)。
- 同源策略太严影响开发,于是有了 CORS(跨域资源共享)。
- 同源策略和 CORS 只防“读”不防“写”,于是催生了 CSRF 攻击。
- 为了防御 CSRF,我们引入了 Token 和 SameSite 机制。
做 Web 开发,既要懂“怎么通”(配置 CORS 解决前端报错),更要懂“怎么堵”(保护后端不被恶意调用)。下次再看到 CORS 报错的红字时,在配置 Allow-Origin 之前,不妨先想一想:你的后端,现在“裸奔”了吗?