這是 Pre-signed URL 學習路線圖 的 Level 0(前置背景)。這一關不講 pre-signed URL 本身,而是把後面每一關都會用到、但很多人一直「大概知道、其實沒真的懂」的 5 個地基詞彙一次補齊:HTTP 方法、header vs query string、私有物件(403)、HMAC、URL encoding。每個都用大白話 + 一個具體小例子講到你真的懂。
這一關學完,你要能用自己的話回答這 5 件事:GET/PUT/HEAD 各是什麼、資訊塞在 header 還是網址 ? 後面差在哪、為什麼直接打一個私有物件網址會被擋(403)、為什麼兩邊不用互傳密碼、卻能證明「我有那把密碼」(HMAC)、以及為什麼「網址編碼差一格、簽章就整個作廢」。抓住這 5 點,Level 1 那句「為什麼點一條網址就能存取私有物件、卻不帶任何帳密」你會一聽就通。
這幾題就是本文的重點,也是讀完你該能用自己的話答出來的問題。先想想現在答不答得出來,然後帶著它們往下讀——遇到答案所在的段落就特別留意;讀完再回來點「看答案」對照。
? 後那串,貼到哪都自帶。
<img src>、丟給 curl,都不用另外設定就能用。Referer、瀏覽器歷史都會留,簽章跟著外洩。+、S3 按 %20 去算 → 兩邊拿去算的輸入就不一樣。SignatureDoesNotMatch,整條 URL 作廢。先講最底層的直覺:一個 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:
GET /bucket/photo.jpg → 「把這張照片下載回來給我」,回應裡有整張圖的 bytes。PUT /bucket/photo.jpg → 「把我這份資料放到這個位置」,若那裡本來就有一張,直接被蓋掉。HEAD /bucket/photo.jpg → 「我只想知道這張圖多大、是什麼格式,先別把整張傳給我」,回應只有 header(如 Content-Length: 20480、Content-Type: image/jpeg),沒有圖的內容。適合「下載前先確認檔案存不存在/多大」。為什麼 Level 0 要先講這個? 因為 pre-signed URL 的本質,就是把「某一個特定 method + 某一個特定物件路徑」這整包請求先簽好。所以一條「簽給 GET」的 URL,拿去做 PUT 會失敗——動詞被簽死了。後面 Level 2 講的「下載連結 vs 上傳連結」,差別就在這裡簽的是 GET 還是 PUT。
一個請求除了「動詞 + 網址」,通常還要夾帶一些額外資訊。這些資訊可以放兩個地方,這個區別是理解 pre-signed URL 的關鍵:
Authorization: ...、Content-Type: image/jpeg)。它不在網址裡,是請求另外附帶的一組 key/value。? 後面那一串。用白話講,它是 URL 裡「path 之後、以 ? 開頭」的部分,格式是「用 & 分隔的一組組 key=value」(MDN 定義即為此)。具體例子,把一條 URL 拆開看誰是誰(用 https://my-host/my-bucket/photo.jpg?X-Amz-Expires=900&X-Amz-Signature=a1b2c3 這條):
| 部位 | 這條 URL 裡對應的內容 | 意思 |
|---|---|---|
| scheme | https:// | 用哪個協定 |
| host | my-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 時的做法,這裡不走那條。
物件儲存(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。
具體例子:
https://my-host/my-bucket/photo.jpg 這條「乾淨」的網址貼給朋友 → 他點開,S3 看懂他要 photo.jpg,但發現他沒有任何授權 → 回 403(AccessDenied),他什麼都拿不到。?X-Amz-...&X-Amz-Signature=...)給他 → 在你設定的期限內,他點開就能下載。期限一過,又變回打不開。AWS 對 pre-signed URL 的定位正是這件事:它讓「識別方式不是 AWS 憑證」的人(例如一個只有瀏覽器、根本沒有 AWS 帳號的訪客)也能在有限時間內存取一個私有物件——關鍵是「這條請求被 IAM 認得(recognized by IAM)」,靠的就是網址裡那段簽章,而不是對方要有帳密。
一句話串起來:私有物件 = 沒授權就 403。pre-signed URL 的整個存在意義,就是「幫一個沒有帳密的人,臨時、限時地把那扇 403 的門打開」。它怎麼在不發帳密的情況下做到「有效授權」?答案是下一段的 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)。它的運作正是比喻裡那兩步:
接回 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、兩邊各算一次、比對是否相同。多層衍生是後面的細節,不影響這個核心心智模型。
先講什麼是 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。)