Cloudfare 缓存引发的 CORS 问题

type
status
date
slug
summary
tags
category
icon
password
AI summary
起因是刷到一个在线摄影集项目 Afilmory ,被其美观干净的 UI 所吸引,所以也想部署一个自己的摄影集网站,虽然买相机至今还没拍摄多少照片……
本着能白嫖就白嫖的原则,使用 Cloudfare R2 作为照片存储桶,Vercel 来部署静态网站。部署项目采用的是本地图片预处理 + 远程静态资源使用的方式,因为项目在构建时需要处理大量图片,这是一个非常消耗计算资源(CPU 和内存)的过程。Vercel 的免费套餐构建环境有时间和内存限制,如果照片数量很多,构建过程很可能会失败。下面就着重讲述一下使用 Cloudfare R2 存储照片、使用并出现问题的过程。

Cloudfare R2 搭建图床

Cloudflare R2 是一款对象存储服务,旨在让开发者能够存储大量非结构化数据,其最核心的特点是免除了其他云存储服务商通常会收取的高昂的出口费用。
R2 提供了一个非常可观的免费套餐,尤其适合个人开发者和小型项目:
  • 每月 10 GB 的免费存储空间。
  • 每月 100 万次免费的写入操作。
  • 每月 1000 万次免费的读取操作。
具体的图床搭建步骤可以参考这篇文章:从零开始搭建你的免费图床系统 (Cloudflare R2 + WebP Cloud + PicGo)之后,对于每张上传的图片,便可以得到可以公共访问的 URL:
notion image
📢
建议绑定一个自定义域名,因为 cloudfare 提供的域名有一定的限制:
This URL is rate-limited and not recommended for production. Cloudflare features like Access and Caching are unavailable. Connect a custom domain to the bucket to support production workloads.
绑定自定义域名需要将域名托管到 cloudfare 上,步骤也很简单,按照提供的信息更换原来域名的域名服务器为 Cloudflare 的域名服务器。同时迁移成功后,所有和域名解析相关的操作都必须在 Cloudflare 的 DNS 控制台进行,原来域名提供商的 DNS 解析设置将不再生效。

CORS 策略

CORS 的全称是 “跨域资源共享” (Cross-Origin Resource Sharing)。它是一种基于 HTTP 头的机制,允许服务器声明,除了它自己的源(域、协议或端口)之外,还有哪些源可以有权限访问它加载的资源

为什么需要 CORS?源于“同源策略”

要理解 CORS,首先必须理解浏览器的 “同源策略” (Same-Origin Policy)。同源策略是浏览器最核心、最基本的安全功能。它规定,一个源的文档或脚本,只能与和它 “同源” 的资源进行交互
如果两个 URL 的 协议 (protocol)、主机 (host) 和 端口 (port) 都相同,那么它们就是同源的。
URL
http://example.com/index.html 的比较
结果
http://example.com/page.html
同源
允许
https://example.com/index.html
不同协议 (https vs http)
跨域
http://www.example.com/index.html
不同主机 (www.example.com vs example.com)
跨域
http://example.com:8080/index.html
不同端口 (8080 vs 80)
跨域
同源策略极大地提高了浏览器的安全性,可以有效防止恶意网站通过脚本轻易地窃取你在其他网站上的数据。但是,跨域请求(比如从 site-a.com 的前端页面请求 api.b.com 的数据)在某些情况下是合理且常见的需求。为了在不完全破坏同源策略安全性的前提下,有控制地允许这些跨域请求,CORS 应运而生。

CORS 是如何工作的?

CORS 的核心思想是:使用新增的 HTTP 头部,让服务器告诉浏览器,它是否允许当前这个源的请求。 整个过程由浏览器自动完成,对前端开发者来说通常是无感的。
  1. 浏览器在请求头中自动加入 Origin 字段,表明请求来自哪个源。
    1. 服务器收到请求后,检查 Origin 字段。如果这个源在许可范围内,服务器就会在响应头中加入 Access-Control-Allow-Origin 字段。
      1. 浏览器接收到响应后,检查 Access-Control-Allow-Origin 字段。如果该字段的值是请求的源,则请求成功,浏览器将响应内容交给 JavaScript。否则,浏览器会拦截响应,并在控制台抛出 CORS 错误。
      📢
      CORS 完全由服务器端控制, 解决 CORS 问题的关键在于正确配置服务器的响应头

      Cloudfare R2 设置 CORS

      构建好站点之后,如果在没有配置 Cloudfare R2 的 CORS 策略情况下,直接打开图片,会得到对应的报错:
      notion image
      打开 Cloudfare R2 并进入创建的存储图,比如我的是 huhu-photo,在 Settings 菜单栏下就有 CORS 配置界面,可以设置对应的 CORS 策略:
      notion image
      我设置的策略如下,并没有限制特定的域名请求:
      • "AllowedOrigins":告诉 R2 允许哪些源(域名)来访问资源,这里是允许公开请求。
      • "AllowedMethods": 允许的 HTTP 请求方法,对于图片对象来说,GETHEADOPTIONS 一般就足够了。
      ⚠️
      问题来了,即使我设置了 CORS 策略,使用了无痕浏览(排除缓存的问题),但是在查看某些图片时依然遭到了拒绝,显示同样的 CORS 错误,而且这些出错的图片不是固定的,同时这是为什么呢?
      询问 AI 之后,尝试使用 curl 验证一下请求返回的 header 部分,来确定 CORS 策略是否正确生效:
      可以看到响应部分确实有 access-control-allow-origin 字段,排除了 CORS 策略的问题。查不到具体的问题,只能去提交个 issue 了,果然 issue 是最好的学习地方,有个佬遇到了同样的问题,并给出了具体的原因,以及完整的复现流程 🤩,具体内容在这里

      cloudfare 缓存导致的请求错误

      💡
      源服务器 R2 只在收到带有 Origin 请求头的请求时,才会在响应里加上 CORS 相关的头部(比如 Access-Control-Allow-Origin)。
      接下来具体阐述一下佬友给出的问题原因和解决方案:
      • 第一次请求(缓存建立):当浏览器第一次请求一张图片时,这个请求可能没有带上 Origin 头(表明这是一个跨域请求)。Cloudflare 的边缘节点收到请求后,发现自己的缓存里没有这张图,于是就去 R2 仓库里把图取了过来。因为这次请求没有跨域信息,所以 R2 返回的图片不包含 CORS 响应头(比如 Access-Control-Allow-Origin)。Cloudflare 拿到这张不带 CORS 头的图片后,于是就把这张图缓存到了自己的服务器上
      • 第二次请求(触发跨域):当网站(gallery.mwwlzz.top)再次请求这张图片时,这次请求是一个明确的跨域请求,浏览器会带上 Origin 头。请求发到 Cloudflare。
      • 问题发生:Cloudflare 在自己的边缘节点上看到了这个请求,发现这张图有缓存。于是,直接把上次缓存的那张不带 CORS 头的图片返回给了浏览器。
      • 浏览器报错:浏览器收到了图片,但发现响应里没有 CORS 响应头,于是出于安全考虑,就拒绝加载图片,并在控制台报出 CORS 错误。
      📢
      源服务器 R2 只在收到带有 Origin 请求头的请求时,才会在响应里加上 CORS 相关的头部(比如 Access-Control-Allow-Origin)。
      这就是为什么问题会随机出现:能否加载成功,取决于请求的那张图片在 Cloudflare 的缓存里是带 CORS 头的版本还是不带 CORS 头的版本。刷新页面可能会清除或替换缓存,导致加载情况发生变化。

      错误的 Cloudflare 缓存如何产生的呢?

      既然浏览器会自动加上 Origin 头,那为什么会没有触发 CORS 呢?哪种情况可能没有带上 Origin 头呢?

      场景一:触发了 CORS 的“标准跨域请求”

      1. 浏览器
          • 网站里的 JavaScript 代码要请求一张图片,图片的域名是 hu-r2.mwwlzz.top
          • 浏览器发现这两个域名不一样,这是一个跨域请求
          • 于是,浏览器在发给 Cloudflare 的请求里,自动加上了一个 Origin 请求头,告诉服务器这个请求来自哪里。
          • 请求头里包含:Origin: https://gallery.mwwlzz.top
      1. Cloudflare
          • 收到这个带 Origin 头的请求。
          • 假设此时缓存里没有这张图,它就把这个完整的请求转发给源服务器 R2。
      1. R2 服务器
          • 收到请求,看到了 Origin 请求头
          • 检查策略,发现 https://gallery.mwwlzz.top 是被允许的。
          • 于是,它在返回图片数据的同时,在响应头里加上了 Access-Control-Allow-Origin: https://gallery.mwwlzz.top
      1. 结果:这份带有 CORS 头部的完整响应被送回 Cloudflare,Cloudflare 可能会缓存它,然后再发给浏览器。浏览器收到后,检查头部发现来源匹配,于是正常显示图片。

      场景二:没有触发 CORS 的“直接请求”(问题的根源)

      这种情况可能在多种情况下发生,最典型的一个例子就是:
      ⚠️
      直接在浏览器地址栏里输入了图片地址 https://hu-r2.mwwlzz.top/image.jpg 并查看图片。(我好像确实干了)
      1. 浏览器直接访问
          • 用户请求的地址和资源地址的域名是同一个 (hu-r2.mwwlzz.top)。
          • 浏览器认为“这不是跨域请求”。
          • 因此,浏览器发送的请求里完全不包含 Origin 这个请求头
      1. Cloudflare
          • 收到这个不带 Origin的请求。
          • 假设缓存是空的,它把这个请求转发给 R2。
      1. R2 服务器
          • 收到请求,检查请求头。
          • 没有找到 Origin 请求头
          • R2 认为这不是跨域请求,没必要加任何 CORS 相关的头部。
          • 于是,它只返回了图片数据和一些基本头部,响应里完全没有 Access-Control-Allow-Origin
      1. 关键时刻:缓存建立
          • Cloudflare 收到了这份不带 CORS 头部的响应。
          • 对于 Cloudflare 来说,这是一份完全有效的响应,它的任务就是提升之后的访问速度,所以它把这份不完整的响应缓存了下来
      现在,Cloudflare 的缓存里有了一份不带 CORS 头部的图片。之后,当通过网站再去请求这张已经被缓存的图片时:
      1. 网站发起了一个带 Origin 头的跨域请求。
      1. 请求到达 Cloudflare。
      1. Cloudflare 在缓存里找到了这张图,直接把上次在场景二中缓存的那份不带 CORS 头部的响应返回给了浏览器。
      1. 浏览器收到了这份响应,发现它需要进行跨域检查,却找不到所需的 Access-Control-Allow-Origin 头部,于是立刻拒绝加载图片,并在控制台报错。

      结论

      Cloudflare 本身是无辜的,它只是一个高效的缓存系统,忠实地缓存从源服务器 R2 收到的东西。问题的根源在于,它缓存了由一个“非跨域请求”所产生的“不带 CORS 头的响应”,然后错误地将这份缓存提供给了一个需要 CORS 头的“跨域请求”。

      设置 Cloudflare 不缓存 R2 资源

      解决方案的核心就是告诉 Cloudflare:凡是访问 R2 图片域名的请求,都不要缓存,每次都去 R2 源服务器拿。 这样就能保证每次请求返回的都是 R2 直接给出的、带有正确 CORS 策略的响应。因此需要创建一个 “缓存规则 (Cache Rule)”
      1. 选择绑定的域名:这里是 mwwlzz.top
      1. 进入缓存规则页面
          • 在左侧的菜单栏中,找到并点击 “Caching”
          • 在下拉菜单中,选择 “Cache Rules”
      1. 创建新规则
          • 点击 “Create rule” 按钮。
      1. 配置规则
          • Rule name: 给规则起一个名字,比如 Bypass Cache for R2
          • When incoming requests match...: 这里是设置规则的触发条件。
            • Field 下拉框中,选择 “Hostname”
            • Operator 下拉框中,选择 “equals”
            • Value 输入框中,填入 R2 绑定的自定义域名 hu-r2.mwwlzz.top
          • Then...: 这里是设置匹配成功后要执行的操作。
            • 找到 Cache eligibility 这个选项。
            • 选择 “Bypass cache”
      1. 保存并部署
      notion image
      规则创建后会立即生效。现在所有访问 hu-r2.mwwlzz.top 的请求都会绕过 Cloudflare 的 CDN 缓存,直接由 R2 处理,按理说彻底解决了间歇性的 CORS 问题。但是,还是会出现之前的问题 🥲。
      接着尝试清楚所有的缓存,同样是在 “Caching” 菜单栏下,找到 “Configuration”,然后点击 “Purge Everything”,即可清除所有的 cloudfare 缓存内容。但,还是不行
      notion image

      Worker 脚本拦截并修改响应

      由于上述方法都不行,最后尝试部署一个 worker,拦截所有发往 R2 域名的响应,然后强行给这些响应加上正确的 CORS 头部,最后再发给浏览器。这样一来,无论 R2 返回的响应是否带 CORS 头,也无论 Cloudflare 的缓存里是什么内容,Worker 都会确保最终浏览器收到的响应一定是带有正确 CORS 头的
      这段代码会做两件事:
      1. 如果收到 OPTIONS 预检请求,它会直接返回正确的 CORS 头部,告诉浏览器“安全,请继续”。
      1. 如果收到 GETHEAD 等实际的图片请求,它会先从 R2(或缓存)获取图片,然后强行在响应中添加上正确的 CORS 头部再返回给浏览器。
      设置路由来触发 Worker
      Worker 创建并部署好代码后,它还不知道要对哪些请求生效。我们需要告诉它。找到 Worker 的管理页面:
      • 点击 “Triggers” 选项卡。
      • “Routes” 部分,点击 “Add route”
      • Route 输入框中,填入 R2 域名,并使用通配符匹配所有路径:hu-r2.mwwlzz.top/*
      • Zone 选择主域名 mwwlzz.top
      • 点击 “Add route” 保存。
      完成以上所有步骤后,这个 Worker 就会自动修改所有对 R2 资源的请求,强行保证 CORS 头部正确无误。至此,也确实彻底解决了之前的问题

      一点感想

      想记录这次解决问题的过程,是因为感触良多。AI 能成为高效的“执行者”,却难成深刻的“洞察者”。当面对复杂未知的问题时,它的能力边界就显现出来。而人的智慧和经验与社区的公开,恰好能弥补这最后的“一公里”。一个有经验的开发者,能根据过往的经验找出问题的关键,这种需要实际工程的经验积累,是 AI 难以复制的。而社区打破了个人认知的孤岛,让分散的智慧得以汇聚和分享。
      最后也是得到了好看的 gallery,欢迎浏览我这还较为青涩的摄影:https://gallery.mwwlzz.top/
      notion image
      Loading...

      © huhu 2023-2025