學習筆記:Pre-signed URL Level 0 — 前置背景

jkopay-member · 學習文章 · 2026-07-07 10:30

這是 Pre-signed URL 學習路線圖Level 0(前置背景)。這一關不講 pre-signed URL 本身,而是把後面每一關都會用到、但很多人一直「大概知道、其實沒真的懂」的 5 個地基詞彙一次補齊:HTTP 方法header vs query string私有物件(403)HMACURL encoding。每個都用大白話 + 一個具體小例子講到你真的懂。

這一關學完,你要能用自己的話回答這 5 件事:GET/PUT/HEAD 各是什麼、資訊塞在 header 還是網址 ? 後面差在哪、為什麼直接打一個私有物件網址會被擋(403)、為什麼兩邊不用互傳密碼、卻能證明「我有那把密碼」(HMAC)、以及為什麼「網址編碼差一格、簽章就整個作廢」。抓住這 5 點,Level 1 那句「為什麼點一條網址就能存取私有物件、卻不帶任何帳密」你會一聽就通。

就愛考你

這幾題就是本文的重點,也是讀完你該能用自己的話答出來的問題。先想想現在答不答得出來,然後帶著它們往下讀——遇到答案所在的段落就特別留意;讀完再回來點「看答案」對照。

  1. GET / PUT / HEAD 各做什麼?為什麼一條「簽給 GET」的 pre-signed URL 拿去做 PUT 會失敗?看答案收合
    GET=取回、PUT=覆蓋上傳、HEAD=只拿 header 不拿內容。
    • GET:把資源讀回來(下載)。
    • PUT:用請求內容整個覆蓋目標位置。
    • HEAD:只回 header、不回 body(先看檔案大小/存不存在)。
    為什麼簽給 GET 的拿去 PUT 會失敗?
    pre-signed URL 把「method+物件路徑」一起簽死。換成 PUT,S3 重算的簽章跟網址裡那個對不上 → 擋下。
  2. 同一份授權資訊放 HTTP header 和放 query string 差在哪?pre-signed URL 為什麼刻意塞進 query string,這是為了比較安全嗎?看答案收合
    差別:header 要另外設定才帶得上;query string 是網址 ? 後那串,貼到哪都自帶。
    • 為什麼塞 query string:純為方便——貼瀏覽器網址列、寫進 <img src>、丟給 curl,都不用另外設定就能用。
    • 不是為了安全,反而更差:網址容易被記下來——access log、Referer、瀏覽器歷史都會留,簽章跟著外洩。
  3. 為什麼直接打一個私有物件的乾淨網址會拿到 403,而不是 401?兩者差在哪?看答案收合
    403=伺服器看懂但拒絕(就是沒權限);401=沒驗證/帳密錯。
    • bucket 預設不公開;匿名、沒帶授權去打私有物件 → 403
    • 403 就算換身分重新登入也沒用——問題不在身分,在權限。
  4. HMAC 為什麼能「兩邊都不用把 secret 傳出去,卻證明你握有 secret」?在 pre-signed URL 裡,負責驗證的是哪一方?看答案收合
    兩邊各有同一把 secret,各自算一次、比對相同才放行——secret 從不出現在網址(只有 access key ID+簽章)。
    • :用 secret 對「這個請求」算出簽章,附上去。
    • S3:用同一把 secret 對「同一份請求」重算,跟收到的簽章比對。
    • 所以驗證方是 S3/MinIO,不是那個拿連結點開下載的人。
  5. 為什麼「一個空格編成 %20 還是 +」這種 URL encoding 差異,會讓整條簽章作廢?看答案收合
    簽章是對請求的每個 byte 算 HMAC,差一個 byte,輸出就整個變。
    • 同一個空格,你編成 +、S3 按 %20 去算 → 兩邊拿去算的輸入就不一樣。
    • 輸入不同 → 兩邊簽章對不上 → 回 SignatureDoesNotMatch,整條 URL 作廢。
  6. (綜合)拿到 pre-signed 連結、點開就能下載的那個人,手上到底有沒有 secret?他扮演什麼角色?看答案收合
    兩把 secret 都沒有。
    • 他只拿著一張已「封印好」的網址,任務是原封不動送到 S3 門口。
    • 真正握有 secret、負責驗封印的,是你(產 URL)+ S3 這兩邊。
    • 這種「憑一張通行證就能過、自己沒任何密鑰」正是 bearer token 的味道。

1. HTTP 方法:GET / PUT / HEAD

先講最底層的直覺:一個 HTTP 請求,本質上就是一句話——「我想對『這個網址』做『某個動作』」。那個「動作」就叫 HTTP method(方法),你可以把它想成句子裡的動詞。網址是受詞,method 是動詞。

pre-signed URL 會用到的三個動詞(白話先講,MDN 定義原文附在括號當佐證):

方法白話它做什麼
GET(下載、把東西讀回來)只把指定的資源讀回來,不該夾帶 body(MDN 原文:requests a representation of the specified resource)
PUT(上傳;同一個位置已有東西就整個覆蓋掉)用請求內容整個取代目標位置現有的資料(MDN 原文:replaces all current representations of the target resource with the request content)
HEAD只看資訊(不下載內容本體)跟 GET 要一模一樣的回應、但不回傳 body,所以拿得到 header、拿不到內容(MDN 原文:asks for a response identical to a GET request, but without a response body)

具體例子,同一個檔案 photo.jpg

為什麼 Level 0 要先講這個? 因為 pre-signed URL 的本質,就是把「某一個特定 method + 某一個特定物件路徑」這整包請求先簽好。所以一條「簽給 GET」的 URL,拿去做 PUT 會失敗——動詞被簽死了。後面 Level 2 講的「下載連結 vs 上傳連結」,差別就在這裡簽的是 GET 還是 PUT。

2. 請求把料放哪:HTTP header vs query string

一個請求除了「動詞 + 網址」,通常還要夾帶一些額外資訊。這些資訊可以放兩個地方,這個區別是理解 pre-signed URL 的關鍵:

具體例子,把一條 URL 拆開看誰是誰(用 https://my-host/my-bucket/photo.jpg?X-Amz-Expires=900&X-Amz-Signature=a1b2c3 這條):

部位這條 URL 裡對應的內容意思
schemehttps://用哪個協定
hostmy-host在哪一台
path/my-bucket/photo.jpg哪個物件
query string?X-Amz-Expires=900&X-Amz-Signature=a1b2c3? 後面這一整串;pre-signed URL 的簽章就藏在這裡

那為什麼要把簽章塞進 query string、而不是放 header?理由只有兩個字:方便。把整份授權都寫進網址本身,這條網址就變成「自帶通行證」——你把它貼給任何人、貼到瀏覽器網址列、寫進 <img src>、丟給 curl,對方什麼都不用另外設定就能直接用。反過來,如果簽章放在 header 裡,用的人就得自己想辦法「多帶一個 header」才打得動;那「貼一條網址就能用」這件事當場破功——瀏覽器網址列、<img src>、隨手一條 curl,這些場合根本沒有讓你順手塞 header 的地方。AWS 官方也點明:pre-signed URL 的簽章就是放在 X-Amz-Signature 這個 query 參數裡一起送出(原文:is transmitted in X-Amz-Signature)。

這裡要先把一個常見誤會講死:把簽章放進網址,並不是為了「比較安全」,恰恰相反。網址是所有東西裡最容易被順手記下來的——伺服器的 access log 會把整條 URL 記下來、你從一個頁面連出去時瀏覽器會透過 Referer 把來源網址送給下一站、瀏覽器歷史紀錄也留著它。簽章一旦躺在網址裡,就跟著這些地方一起被記下來、多了外洩的破口,所以放 query string 在資安上其實更差。這裡選它,純粹是拿「一點安全」去換「一條網址走天下」的方便——這筆帳怎麼算、風險怎麼收,留到 Level 5 談安全時細講。順帶一提,簽章也能放進 header,那是一般程式直接呼叫 API 時的做法,這裡不走那條。

3. 什麼叫「私有物件」,以及 403 是什麼意思

物件儲存(S3 / MinIO)的 bucket 預設是不公開的。也就是說,一個路過的陌生人、拿著物件的網址、但沒有帶任何有效授權,直接去打——會被擋下來,收到一個 HTTP 403 Forbidden

403 到底什麼意思?MDN 的定義是:伺服器看懂你要幹嘛了,但拒絕幫你做(原文:The server understood the request but refused to process it)。重點來了——它跟「你帳密打錯」(那是 401)不同:403 是「就是不給你這個權限」,就算你重新登入、換個身分再驗一次也一樣沒用(原文:authenticating or re-authenticating makes no difference … tied to … insufficient permissions)。對一個沒帶授權的匿名請求來說,私有物件的回應就是 403。

具體例子

AWS 對 pre-signed URL 的定位正是這件事:它讓「識別方式不是 AWS 憑證」的人(例如一個只有瀏覽器、根本沒有 AWS 帳號的訪客)也能在有限時間內存取一個私有物件——關鍵是「這條請求被 IAM 認得(recognized by IAM)」,靠的就是網址裡那段簽章,而不是對方要有帳密。

一句話串起來:私有物件 = 沒授權就 403。pre-signed URL 的整個存在意義,就是「幫一個沒有帳密的人,臨時、限時地把那扇 403 的門打開」。它怎麼在不發帳密的情況下做到「有效授權」?答案是下一段的 HMAC。

4. 對稱金鑰與 HMAC:為什麼「不用互傳密碼」也能證明你有密碼

這一段是整個 Level 0 的靈魂。先別碰任何密碼學術語,我們用一個生活比喻。

暗號本的比喻。 你和一個收件方(負責檢查的人),事先一人一本、拿到一模一樣的暗號本(這就是「對稱金鑰/同一把 secret」——兩邊各有一份、內容相同)。

今天你要寄一張明信片給這個收件方,你怕別人半路改字、也怕別人冒充你。於是你把明信片上那句話,照暗號本的規則算出一個「封印碼」,寫在明信片角落。

收件方收到後,拿出他手上那本一樣的暗號本,對著明信片上「同一句話」再算一次封印碼,然後比對:算出來的碼,和你寫在角落的碼,一不一樣?
・一樣 → 這張明信片一定是「有暗號本的人」寫的(別人沒本、算不出對的碼),而且一個字都沒被改過(改了字,重算的碼就對不上)。
・不一樣 → 拒收。

整個過程裡,你從頭到尾沒有把暗號本本身寄出去——因為根本不需要,你們兩邊本來就各有一份。

把比喻換成正式名詞,這就是 HMAC(Hash-based Message Authentication Code)。用 RFC 2104 的話講,它是「用雜湊函式 + 一把雙方共享的秘密金鑰來做訊息驗證的機制」(原文:a mechanism for message authentication using cryptographic hash functions … in combination with a secret shared key)。它的運作正是比喻裡那兩步:

flowchart TB subgraph SND["寄件方 · 你(產 URL)"] direction LR K1["你手上的
共享 secret"] --> S1["對請求內容
算 HMAC"] --> SIG["得到簽章"] end SIG ==>|"簽章塞進網址、一起送出"| V["S3 / MinIO 收到請求"] subgraph RCV["驗證方 · S3 / MinIO"] direction LR K2["S3 手上
同一把 secret"] --> S2["對『同一份請求內容』
重算 HMAC"] S2 --> CMP{"重算的簽章
= 收到的?"} CMP -->|一樣| OK["✅ 放行"] CMP -->|不一樣| NO["❌ SignatureDoesNotMatch"] end V ==> S2

接回 pre-signed URL: 比喻裡那個「收件方/負責重算比對的人」,對應的其實是 S3/MinIO 這個驗證方——不是你把連結傳過去的那個下載者。你(產 URL 的人)和 S3/MinIO(驗 URL 的人)共用同一把 secret access key——這就是那本「兩邊各一份的暗號本」。你用它對「這個請求」算出簽章,塞進網址的 X-Amz-Signature。S3 收到後,用它那份一樣的 secret 對「同一個請求」重算一次簽章、比對。一樣才放行。

那第 3 段裡「拿到連結、點開下載」的那個人呢?他其實兩把 secret 都沒有——他手上只有一張你已經「封印好」的明信片(帶著簽章的網址),任務就是把它原封不動送到 S3 門口。真正握有 secret、負責驗封印的,始終是你和 S3 這兩邊。(這種「憑一張已封印的通行證就能過關、自己不需要任何密鑰」的味道,正是 Level 1 要正式講的 bearer token,先記著。)

關鍵洞見來了:那把 secret 從頭到尾都沒出現在網址裡。網址裡出現的只有「access key ID」(相當於「我用的是哪一本暗號本」的編號,不是暗號本內容)和「算出來的簽章」。AWS 講得很直接:一條 pre-signed 請求裡唯一「天生就是秘密」的東西,就是那段簽章(原文:the only inherently secret component … is its Signature Version 4 signature)。這就是「不用把密碼傳出去,也能證明你握有密碼」的原理——因為驗證方是「自己用同一把 secret 重算並比對」,而不是「拿你傳來的密碼去核對」。

誠實標註(這點我簡化了,細節留給 Level 3): SigV4 實際上不是直接拿 secret access key 去做一次 HMAC,而是先用它「衍生」出一把只對某天/某 region/某服務有效的 signing key(連做 4 次 HMAC),再拿這把衍生 key 去簽。這是為了限制金鑰外洩的爆炸半徑。但 Level 0 你只要牢牢抓住這個直覺就夠:同一把共享 secret、兩邊各算一次、比對是否相同。多層衍生是後面的細節,不影響這個核心心智模型。

5. URL encoding(百分號編碼):為什麼「差一格、簽章就整個作廢」

先講什麼是 URL encoding。網址裡有些字元「有特殊職務」:/ 分隔路徑、? 開啟 query、& 分隔參數,還有空格根本不能直接出現在網址裡。當你想把這些字元當成普通文字內容放進網址(例如檔名裡真的有空格),就得先把它「翻譯」成 % + 該字元 ASCII 的十六進位——例如空格會變成 %20。這就是 percent-encoding(URL encoding)(MDN 原文:a mechanism to encode … characters that have specific meaning in the context of URLs)。

這裡有個常見小坑:同樣是「空格」,一般網址那套會編成 %20,但 HTML 表單那套(x-www-form-urlencoded)會編成 +。兩種看起來都「像有編碼」,其實是不同的 byte。

為什麼這對簽章是生死攸關的? 回想第 4 段:簽章是對「請求內容」算 HMAC,而 HMAC 有個特性——輸入只要差一個 byte,輸出就整個天翻地覆。所以只要你這邊把空格編成 +、S3 那邊卻按 %20 去重算,兩邊拿去算 HMAC 的輸入就不是同一份 → 兩邊算出的簽章對不上 → server 直接回 SignatureDoesNotMatch,整條 URL 作廢。差的只是「一個空格怎麼編」,簽章就全毀。

一句話收尾:簽章對「一模一樣的 byte」極度敏感,URL encoding 只要你這邊和 server 那邊對不齊——哪怕只是一個空格的編法差異——簽章就對不上、URL 就失效。這也是為什麼實務上「自己手刻 pre-signed URL」很容易踩雷,通常直接用 SDK(Level 4 會教)讓它幫你把編碼處理到一致。(更細的編碼規則——像是要先把整個請求排成標準字串再簽、路徑與 query 的編碼差異——留到 Level 3。)

參考來源(實際爬過的權威連結)