跳到主要内容
AnyStorage
免费下载

Amazon S3

S3 CORS 报错:修好 Access-Control-Allow-Origin

浏览器访问 Amazon S3 报 CORS,是桶配置问题而不是代码 bug。五个配置元素、S3 返回的两种 403,以及用 curl 验证的方法。

S3 返回的两种 CORS 403 各代表什么、规则怎么匹配、AllowedOrigins 与 AllowedHeaders 的通配符上限,以及 ETag 为什么必须写进 ExposeHeaders。

S3 CORS 报错,S3 CORS 配置,S3 Access-Control-Allow-Origin

你的 fetch 没写错,凭证也没错,同一个对象用 curl 能顺利下载。是浏览器不肯把响应交给你的 JavaScript,因为桶没说「可以交」。以下内容在 2026 年 9 月对照 Amazon S3 用户指南、AWS CLI 参考和 MDN 核对过。Cloudflare 那一侧的同一个问题写在 R2 CORS 报错,两者的差别足以分开读。

浏览器含糊,S3 具体

控制台里出现的是一个笼统的失败。Chrome 是这么写的:

text

Access to fetch at 'https://my-bucket.s3.eu-west-1.amazonaws.com/key.png'
from origin 'http://localhost:3000' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

Firefox 的措辞不同。MDN 把它记录为「Reason: CORS header 'Access-Control-Allow-Origin' missing」,并解释「The response to the CORS request is missing the required Access-Control-Allow-Origin header, which is used to determine whether or not the resource can be accessed by content operating within the current origin.」两者都只说了「缺」,而缺的原因有好几种。

S3 更有用,因为它返回两种可区分的错误。第一种:

text

HTTP/1.1 403 Forbidden
CORS Response: CORS is not enabled for this bucket.

意思是桶上根本没有 CORS 配置。第二种:

text

HTTP/1.1 403 Forbidden
CORS Response: This CORS request is not allowed.

意思是有配置,但和你的请求不匹配。AWS 把第二种的原因收敛成三条:「Origin is not allowed」「Methods are not allowed」「Requested headers are not allowed」。这两句话你在浏览器控制台里永远看不到——CORS 失败的响应体被浏览器藏起来了。用 curl 就能看到,这也是本页后面那个测试成为最短路径的原因。

按症状定位要改的元素
症状S3 在说什么要改的元素
CORS is not enabled for this bucket没有配置新建一份
GET 正常,PUT 报 403方法未列出AllowedMethods
一加 JSON 内容类型就 403请求头未列出AllowedHeaders
上传成功但 etagnull响应头未公开ExposeHeaders
example.com 能用,www.example.com 不行来源要完全匹配AllowedOrigins
刚改完又立刻失败预检被缓存MaxAgeSeconds

S3 怎么挑规则

S3「uses the first CORSRule rule that matches the incoming browser request」,而一条规则只在下面三点全部成立时才算匹配:

  1. 请求的 Origin 头与 AllowedOrigins 中某一项一致。
  2. Access-Control-Request-Method 里的方法在 AllowedMethods 中。
  3. Access-Control-Request-Headers 里列出的每一个头都在 AllowedHeaders 中。

先匹配者胜,所以把宽规则放在窄规则上面,下面那条就永远用不上。一份配置最多 100 条规则,而在 S3 控制台里「the CORS configuration must be JSON」——XML 形式在 API 和 SDK 里仍然能用,控制台不收。另外 ACL 和桶策略照旧生效:CORS 决定的是浏览器能不能读响应,不是谁有权限。

五个元素,以及通配符的算法

AllowedMethods 只接受五个值:GETPUTPOSTDELETEHEAD。没有 OPTIONS,因为预检是 S3 来回答的,不是你来放行的。

配置悄悄失效的地方是通配符。AllowedOrigins 的每一项「can contain only one * wildcard character, such as http://*.example.com」,AllowedHeaders 的每个字符串也「can contain at most one * wildcard character」,官方举的例子是 x-amz-*(放行所有 Amazon 专有头)。排错页里还写了 AllowedMethods 中的 * 会匹配所有 HTTP 方法——只看那五个值的列表是看不出来的。

一份同时覆盖浏览器上传和读取的配置长这样:

json

[
  {
    "AllowedOrigins": ["https://app.example.com", "http://localhost:5173"],
    "AllowedMethods": ["GET", "HEAD", "PUT", "POST"],
    "AllowedHeaders": ["Content-Type", "x-amz-*"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3000
  }
]

MaxAgeSeconds 是浏览器能按「as identified by the resource, the HTTP method, and the origin」缓存预检结果的秒数。设得太大,你改完并部署了也像没生效;排查期间设为 0,收尾时再调回来。

没人记得公开的那个头:ETag

这是最常见的「只好了一半」的 CORS 配置。上传返回 200,对象确实在桶里,可是代码读不到它要的值。AWS 说得很直接:「if you want to read the ETag header from a PUT or multipart upload, you need to include the ExposeHeader tag in your configuration」,以及「The SDK can only access headers that are exposed through CORS configuration.」

浏览器端的分片上传依赖这一点:完成分片上传需要把每个分片的 ETag 回传。没有 "ExposeHeaders": ["ETag"],分片能上去,只有完成那一步会失败。自定义元数据同理,它们以 x-amz-meta-* 头返回,也必须列出来。

localhost、预览地址与带凭证的请求

AllowedOrigins 的项除了那一个通配符之外是按字符串比较的。http://localhost:3000http://localhost:5173 是两个来源,http://localhosthttps://localhost 又是两个,末尾加斜杠则谁都匹配不上。预览环境是「昨天还好的」经典原因:每次部署换一个主机名,一行 https://*.example.dev 这样的通配符就能解决。

还有两条限制来自浏览器而不是 S3。MDN 对带凭证请求的规定是绝对的:服务器「must not specify the * wildcard for the Access-Control-Allow-Origin response-header value, but must instead specify an explicit origin」。而能免掉预检的只有简单请求:GETHEADPOST 且只带安全列表内的头(AcceptAccept-LanguageContent-Language、限于三种 MIME 类型的 Content-Type、以及 Range)。

动手改代码前先用 curl 验预检

AWS 自己给了命令,不必靠猜:

sh

curl -v -X OPTIONS \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: PUT" \
  -H "Access-Control-Request-Headers: content-type" \
  "https://my-bucket.s3.eu-west-1.amazonaws.com/key.png"

配置正确时会返回 200 OK,带上 Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers,以及 Vary: Origin, Access-Control-Request-Headers, Access-Control-Request-Method。同一页上的一句警告能解释很多困惑:「When sending a preflight request, if any of the CORS request headers are not allowed, none of the response CORS headers are returned.」看起来什么都没返回,往往不是配置整体缺失,而是请求里有一项被拒了。

把配置下发

控制台路径:General purpose buckets → 目标桶 → PermissionsCross-origin resource sharing (CORS)Edit,粘贴 JSON,Save changes

能留在仓库里的是 CLI 版:

sh

aws s3api put-bucket-cors --bucket my-bucket --cors-configuration file://cors.json
aws s3api get-bucket-cors --bucket my-bucket

调用方需要 s3:PutBucketCORS 权限,桶所有者默认就有。如果桶前面挂了 CDN,那一层也要配:允许 OPTIONS,转发 OriginAccess-Control-Request-HeadersAccess-Control-Request-Method,并把来源头放进缓存键。AWS 警告说「caching proxies that don't include the origin header in their cache key may serve cached responses that don't include the appropriate CORS headers for different origins」。

预签名 URL 也不例外

预签名 URL 带的是授权,不是读取响应的许可。浏览器照样做 CORS 检查,所以从 JavaScript 发出的预签名 PUT 需要同一份桶配置。排查时记两个期限数字,因为过期的 URL 会返回一个看起来很像 CORS 失败的 403:S3 控制台上限 12 小时,aws s3 presign 上限 7 天。签名那一侧写在 S3 预签名 URL 生成器

什么时候 CORS 不是问题所在

CORS 是保护浏览器的机制,所以它只约束浏览器。桌面客户端自己给请求签名,不发 Origin 头,任何桶配置都拦不住它——这也是为什么一个 Web 应用用不了的桶,同一分钟里在应用里能正常打开。

AnyStorage 0.2.25(macOS 11+、Windows 10+、Ubuntu 20.04+)用一对 access key、可选的自定义端点 URL 和显式的 path-style 开关连接 S3 与 S3 兼容端点,并通过本地 WebDAV 服务器而非内核驱动进行挂载。免费版两个连接,挂载只读。它适合把文件搬走,对你的 Web 应用没有任何帮助——后者仍然需要上面那份 JSON。另见 S3 GUI 客户端总览Windows 版

常见问题

能不能把 AllowedOrigins 直接设成 "*"?

对一个放公开素材的桶来说,["*"] 加上你实际用到的方法是说得过去的。但请求一旦带上凭证就不能这么做了,因为浏览器禁止带凭证的响应使用通配符 Access-Control-Allow-OriginAllowedHeaders 仍请写明:每个字符串最多一个通配符,而多数情况下你想要的其实是 x-amz-*

改了为什么不生效?

几乎总是预检被缓存了。浏览器会按资源、方法、来源这三者的组合在 MaxAgeSeconds 内复用上一次的答案。用 curl -X OPTIONS 看桶现在到底回什么,然后在新的隐私窗口里重试。

服务端上传也需要 CORS 吗?

不需要。CORS 只适用于浏览器 JavaScript 发出的请求。Lambda 函数、Node 进程、CI 任务、桌面客户端都不受它约束——这就是同一套凭证在一处可用、在另一处失败的原因。

一个桶最多能放多少条 CORS 规则?

100 条,且第一条匹配的规则生效。顺序有意义:把具体规则放在宽泛规则之上,否则宽的那条会把本该另行处理的请求接走。

上传成功但读不到 ETag,这是 CORS 吗?

是,而且一个数组就能解决:给匹配的那条规则加上 "ExposeHeaders": ["ETag"]。浏览器端的分片上传没有它就无法完成,你想读的任何 x-amz-meta-* 头也要照同样方式列出。