跳到主要內容
AnyStorage
免費下載

Amazon S3

S3 CORS 錯誤:修好 Access-Control-Allow-Origin

瀏覽器存取 Amazon S3 出現 CORS,是儲存桶設定的問題而不是程式碼的錯。五個設定元素、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-* 標頭也要照同樣方式列出。