小白也能懂的跨域保护:为什么浏览器会报 CORS 错误?

你看到的 CORS error,不是“防病毒软件”在拦截,也不是后端坏了。它是浏览器的一套安全规则,核心目标是:

防止一个网站偷偷读取另一个网站里的用户数据。

严格说,大家口头说的“跨域保护”主要包括两层:

  1. 同源策略:Same-Origin Policy,简称 SOP。默认不让网页脚本随便读取其他来源的数据。
  2. CORS:Cross-Origin Resource Sharing。它不是“更严格的拦截”,而是“在安全前提下允许跨域访问”的标准机制。

MDN 对同源策略的定义是:浏览器限制一个来源加载的文档或脚本,如何与另一个来源的资源交互。所谓“同源”,要求协议、域名、端口都一样。比如 http://localhost:5176http://localhost:8000 端口不同,所以不是同源。来源:MDN Same-origin policy、MDN CORS 文档。

一、如果没有跨域保护,会发生什么?

想象你已经登录了银行网站https://bank.example,浏览器里有银行网站的登录 Cookie。然后你又打开了一个恶意网站https://evil.example

如果浏览器没有同源限制,恶意网站可以在网页里写 JavaScript:

fetch("https://bank.example/api/account")
  .then(res => res.json())
  .then(data => sendToAttacker(data));

问题在于:浏览器请求 https://bank.example 时,可能会自动带上你在银行网站的 Cookie。银行后端看到 Cookie,以为这是你本人正常访问,于是返回账户数据。恶意网站如果能读取响应,就能把你的账户信息发走。

这就是跨域保护要解决的核心问题:浏览器既是用户访问网页的工具,又保存着用户在很多网站的登录状态。如果任意网页都能读取任意网站的数据,整个 Web 就没有隐私边界。

二、人们以前尝试过什么?为什么不够好?

Web 发展早期,大家既需要安全,又需要跨站协作。比如:

  • 前端页面在 example.com
  • API 在 api.example.com
  • 图片、字体、脚本在 CDN
  • 第三方支付、地图、登录服务都来自别的域名

如果完全禁止跨域,现代 Web 很多功能都做不了。所以人们尝试过一些办法。

1. 完全不限制:安全灾难

这是最直观但最危险的方案。任何网页都能读取其他网站的数据,恶意网站就可以利用用户已有登录状态窃取信息。

所以浏览器最终必须有一个默认安全边界。

2. 完全禁止跨域:功能不够用

如果浏览器严格禁止所有跨域行为,那么前端就不能直接请求第三方 API,CDN、公共接口、前后端分离开发都会很麻烦。

这也不符合 Web 的开放性。

3. JSONP:能用,但很危险

CORS 普及前,前端常用 JSONP 跨域取数据。它利用了一个历史规则:

<script src="https://api.example.com/data?callback=handleData"></script>

浏览器允许跨域加载 <script>,于是服务器返回:

handleData({ "name": "Alice" });

这样前端就拿到了数据。

但 JSONP 的本质是:把远程返回内容当 JavaScript 执行。这很危险,因为对方服务器如果返回恶意脚本,它就直接在你的页面里运行。JSONP 还主要适合 GET 请求,不适合复杂 API 权限控制。W3C CORS 标准的安全说明里也提到,CORS 是为了替代 JSONP、服务端代理、跨文档消息等旧式跨域共享办法。

4. 后端代理:能控制,但成本更高

还有一种办法是让自己的后端去请求第三方 API:

浏览器 -> 自己后端 -> 第三方 API

这样浏览器没有直接跨域,安全边界更清楚。但它会增加后端负担,也要求服务端正确处理鉴权、缓存、限流和错误。

后端代理不是失败方案,只是它不适合作为浏览器跨域访问的通用标准。

三、人们如何想到“跨域保护”这套设计?

关键想法是:给网页划分“来源”。

一个来源由三部分组成:

协议 + 域名 + 端口

例如http://localhost:5176,来源是:

协议:http
域名:localhost
端口:5176

下面这些都不是同源:

https://localhost:5176   # 协议不同
http://127.0.0.1:5176    # 域名不同
http://localhost:8000    # 端口不同

同源策略的设计思路是:默认情况下,一个来源的脚本不能读取另一个来源的敏感响应。

但 Web 又确实需要跨域访问,所以后来有了 CORS。CORS 的设计思路是:默认禁止,由资源拥有者明确声明哪些来源可以访问。

比如后端返回:

Access-Control-Allow-Origin: http://localhost:5176

我这个 API 允许 http://localhost:5176 的网页读取响应。如果浏览器发现请求来源是 http://localhost:5176,响应里也允许它,就放行。否则就报 CORS 错误。

对于复杂请求,比如带自定义 header、PUTDELETE、某些 POST,浏览器还会先发一个预检请求:

OPTIONS /api/user
Origin: http://localhost:5176
Access-Control-Request-Method: DELETE

后端如果允许,再返回:

Access-Control-Allow-Origin: http://localhost:5176
Access-Control-Allow-Methods: DELETE

然后浏览器才会发真正的请求。

四、CORS 到底保护了什么?不保护什么?

它主要保护:

  • 恶意网页读取你在其他网站的私密数据
  • 未被授权的前端读取 API 响应
  • 某些复杂跨域请求在真正发送前被浏览器拦下

但它不等于万能安全机制。

CORS 不负责防病毒

病毒通常是操作系统或文件层面的恶意程序。CORS 是浏览器里的 Web 安全边界。

CORS 不等于登录鉴权

就算你配置了 CORS,后端仍然必须检查:

  • 用户是否登录
  • token 是否有效
  • 用户是否有权限访问这条数据

CORS 不完全等于 CSRF 防护

有些跨站请求仍可能被发送,只是浏览器不让恶意网页读取响应。比如传统 HTML 表单提交在历史上就是允许跨站发送的。

所以重要操作仍然需要:

  • CSRF token
  • SameSite Cookie
  • 检查 Origin / Referer
  • 服务端权限判断

五、如何正确使用跨域保护?

例子 1:本地开发只允许本地前端

如果你的前端跑在:

http://localhost:5176

后端跑在:

http://localhost:8000

后端可以配置:

CORS_ORIGINS=http://localhost:5176

这表示只允许这个前端来源访问后端 API。

例子 2:生产环境只允许真实前端域名

如果线上前端是:

https://app.example.com

后端 API 是:

https://api.example.com

后端应该返回:

Access-Control-Allow-Origin: https://app.example.com

而不是:

Access-Control-Allow-Origin: *

尤其是涉及登录态、Cookie、用户数据的 API,更不能随便开放给所有来源。

例子 3:公共资源可以用 *

如果资源本来就是公开的,比如公共字体、公开 JS 库、公开图片资源,可以允许任意来源读取:

Access-Control-Allow-Origin: *

例子 4:带 Cookie 的请求要更谨慎

如果前端请求需要带 Cookie:

fetch("https://api.example.com/me", {
  credentials: "include"
});

后端不能这样:

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

正确做法是明确指定可信来源,并且后端仍然要检查用户登录状态和权限。

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

例子 5:不要“反射”所有 Origin

有些后端会这样写逻辑:

res.setHeader("Access-Control-Allow-Origin", req.headers.origin);

这相当于来谁都信谁。恶意网站请求时带:

Origin: https://evil.example

后端就返回:

Access-Control-Allow-Origin: https://evil.example

这样 CORS 就失去意义了。

正确做法是维护白名单:

const allowedOrigins = [
  "https://app.example.com",
  "http://localhost:5176"
];

const origin = req.headers.origin;

if (allowedOrigins.includes(origin)) {
  res.setHeader("Access-Control-Allow-Origin", origin);
}

例子 6:不要信任不安全的 HTTP 来源

如果你的站点是 HTTPS,却允许:

Access-Control-Allow-Origin: http://app.example.com

那中间人攻击者可能篡改 HTTP 页面,再利用这个被信任的来源访问你的 HTTPS API。本地开发可以用 http://localhost,但线上生产环境应尽量使用 HTTPS。

参考资料