學習筆記:Pre-signed URL Level 4 — 在 .NET 實作(AWS SDK / MinIO SDK)

jkopay-member · 學習文章 · 2026-07-07 14:23
這是 pre-signed URL 學習路線圖的 Level 4,最實用的一站。Level 1~3 已把理論講透:它是什麼、為什麼點了就能存取私有檔卻不帶帳密(Level 1)、下載用 GET 上傳用 PUT(Level 2)、SigV4 那段簽章怎麼算怎麼驗(Level 3)。這篇不重講理論——只把那套理論落地成 .NET 你真的會敲的 code,並用 curl 親手驗一條 URL 能不能動。

前三站你已經「懂」了 pre-signed URL。這一站要讓你「會做」——打開 IDE,用 SDK 產出一條真的能下載、能上傳的網址,再用 curl 證明它真的通。

路線有兩條:官方 AWS SDK for .NET,以及 MinIO 官方 .NET SDK。讀完你要能用兩套各產一條 GET 與 PUT URL、用 curl 成功收發、說清兩套差在哪、以及怎麼把 AWS SDK 指向 MinIO。

就愛考你

這幾題就是本文重點,也是讀完你該能用自己的話答出來的問題。先想想現在答不答得出來,帶著它們往下讀——讀完再回來點「看答案」對照。

  1. 用 AWS SDK 產的 URL 跟用 MinIO SDK 產的 URL,能不能混著用(AWS 產的打 MinIO、MinIO 產的打 S3)?選哪套 SDK 是「相容問題」嗎?
    看答案 能混用,不是相容問題。兩套產出的都是一模一樣的標準 SigV4 URL(就是那串 X-Amz-Algorithm / Credential / Date / Expires / SignedHeaders / Signature)。所以 AWS SDK 產的 URL MinIO 認得、MinIO SDK 產的 URL 真 S3 也認得。選哪套只是「習慣/生態」問題。
  2. 用 AWS SDK 產 URL 的核心類別是哪個?同一個請求物件,怎麼從「下載」切成「上傳」?
    看答案 核心類別是 GetPreSignedUrlRequest,填好屬性後交給 s3Client.GetPreSignedURL(req)。切動作靠 Verb 屬性:不填預設就是 HttpVerb.GET(下載);要上傳就設 Verb = HttpVerb.PUT
  3. 要讓 AWS SDK 對 MinIO 產 URL,需要改 GetPreSignedUrlRequest 的 code 嗎?該設哪兩個東西?ForcePathStyle 又是幹嘛?
    看答案 不用改產 URL 的 code,只改建 client 那一步。AmazonS3Config 上設兩個:ServiceURL(指向 MinIO 位址,如 http://localhost:9000)、ForcePathStyle = trueForcePathStyle 是強制用 path-style 定址(bucket 放路徑裡 endpoint/my-bucket/photo.jpg),而不是把 bucket 塞進 host——MinIO 這類自架 store 幾乎都吃 path-style(回扣 Level 3 全程用的就是它)。
  4. MinIO SDK(v7)設「有效期限」的方式,跟 AWS SDK 最大的差別是什麼?要 15 分鐘各該怎麼填?
    看答案 型別不同。MinIO SDK v7 的 presigned 方法吃 TimeSpan,15 分鐘寫 TimeSpan.FromMinutes(15);AWS SDK 的 Expires絕對時間點(DateTime),15 分鐘寫 DateTime.UtcNow.AddMinutes(15)。一個是「一段長度」、一個是「到哪個時間點為止」。
  5. curl 驗一條當初簽了 content-type 的 PUT URL,為什麼一定要帶對 -H "Content-Type: ..."?帶錯會怎樣?
    看答案 因為 content-type 一旦在產 URL 時被填入,就會被簽進 SignedHeaders。所以上傳時送的 Content-Type 值必須一字不差;漏了或寫錯(如 image/png)會回 SignatureDoesNotMatch。這正是 Level 3 那句「被簽進去的東西改一個字元、簽章就對不上」的實際體現。反之若沒簽 content-type,就別硬加。
  6. MinIO SDK 的版本會影響 presigned 的寫法嗎?動手前該以什麼為準?
    看答案 會,而且差很多。6.x 跟 v7 的寫法明顯不同(詳見內文路線 B 的 caveat)。一律以你專案鎖定的 NuGet 版本為準,別照抄別篇的版本細節。

兩條路,產物是同一個東西

在 .NET 產 pre-signed URL 有兩條主流路線:

先講一個最重要、也最讓人安心的事實:兩條路產出的都是一模一樣的標準 SigV4 URL。就是 Level 3 拆過的那串 X-Amz-Algorithm / X-Amz-Credential / X-Amz-Date / X-Amz-Expires / X-Amz-SignedHeaders / X-Amz-Signature

所以這兩套 SDK 對 AWS S3、對 MinIO、對 Cloudflare R2 都能用,差別只在你把 endpoint 指去哪。(endpoint 就是服務的網址入口——真 S3 是 s3.amazonaws.com,MinIO 是你自己架的那台,例如 http://localhost:9000。)這也回扣了整條路線的核心認知:pre-signed URL 是 S3/SigV4 的公開標準,不是誰家的專屬招式(完整可遷移性是 Level 6 的題目)。

一句話抓重點:選哪套 SDK 是「習慣問題」,不是「相容問題」。用 AWS SDK 也能對 MinIO 產 URL,用 MinIO SDK 產的 URL 拿去打 AWS S3 也認得——因為它們吐出來的都是同一種標準簽章網址。

路線 A:AWS SDK for .NET(AWSSDK.S3)

核心只有一個類別:GetPreSignedUrlRequest。你填幾個屬性,交給 client 的 GetPreSignedURL(...) 就拿到一條 URL。

先認識這幾個關鍵屬性(都是 GetPreSignedUrlRequest 上的直接屬性,經 AWS SDK for .NET V3 API 文件確認):

屬性型別作用
BucketNamestring哪個 bucket
Keystring物件在 bucket 裡的路徑/檔名
VerbHttpVerb動作,HttpVerb.GETHttpVerb.PUT預設就是 GET
ExpiresDateTime到期的「絕對時間點」,通常寫 DateTime.UtcNow.Add...
ContentTypestring(選填)綁定上傳時的 content-type

注意 Expires 是一個絕對時間點(DateTime,不是「幾秒後過期」。這是它跟路線 B 最容易搞混的地方,記住這裡填的是「到什麼時候為止」。

先建一個 AmazonS3Client(連真 S3)

下面的範例都會用到一個 s3Client;連真 AWS S3、指定 region,這樣建:

var s3Client = new AmazonS3Client("accessKey", "secretKey", RegionEndpoint.APNortheast1);

region 是 AWS 的機房區域APNortheast1 就是東京,代號 ap-northeast-1)——也就是 Level 3 X-Amz-Credential 那串裡的「區域」欄位。因為簽章跟區域是綁在一起算的,產 URL 前得先告訴 SDK 你用哪個 region。(指到 MinIO 時 region 通常隨便填、慣例填 us-east-1 即可。)

憑證也可以不寫死、走預設憑證鏈(例如放環境變數),這裡先不展開。(要改成連 MinIO,只換建 client 這步,見本節最後。)

GET:產一條下載連結

Verb 不填就是 GET,最精簡:

// 下載連結(presigned GET),15 分鐘後過期
var req = new GetPreSignedUrlRequest {
    BucketName = "my-bucket",
    Key        = "photo.jpg",
    Expires    = DateTime.UtcNow.AddMinutes(15)   // Verb 省略 = GET
};
string url = s3Client.GetPreSignedURL(req);

PUT:產一條上傳連結(可綁 content-type)

上傳要把 Verb 改成 HttpVerb.PUT。這裡示範多綁一個 ContentType

// 上傳連結(presigned PUT),10 分鐘後過期,且限定對方送 image/jpeg
var putReq = new GetPreSignedUrlRequest {
    BucketName  = "my-bucket",
    Key         = "uploads/photo.jpg",
    Verb        = HttpVerb.PUT,
    Expires     = DateTime.UtcNow.AddMinutes(10),
    ContentType = "image/jpeg"   // 綁 content-type
};
string uploadUrl = s3Client.GetPreSignedURL(putReq);

一旦你在這裡填了 ContentType,它就會被簽進去(進入 Level 3 講的 SignedHeaders)。那對方用這條 URL 上傳時,HTTP 的 Content-Type header 就必須送一模一樣的值,否則會被拒。這個「簽了就得對上」的坑,等下 curl 那節會實際踩一次;為什麼會這樣,回頭看 Level 3「改任一被簽參數就 SignatureDoesNotMatch」就懂了。(要不要綁、綁了的安全含意,是 Level 5 的題目。)

需要非同步版本就用 GetPreSignedURLAsync(request),參數與行為一致。

把 AWS SDK 指到 MinIO:只改建 client 那一步

同樣的 GetPreSignedUrlRequest code 一個字都不用改,只在建 AmazonS3Client 時換設定,就能對 MinIO 產 URL:

var config = new AmazonS3Config {
    ServiceURL     = "http://localhost:9000",  // 指向你的 MinIO endpoint
    ForcePathStyle = true                      // 用 path-style 定址
};
var s3Client = new AmazonS3Client("accessKey", "secretKey", config);

兩個關鍵設定(都在 AmazonS3Config 上,經 AWS SDK for .NET V3 API 文件確認):

路線 B:MinIO .NET SDK(Minio)

MinIO 官方 SDK 的兩個核心方法名很直覺(經 minio-dotnet v7 原始碼 IMinioClient 確認):

下面用現行最新的 v7(NuGet Minio 7.0.0,2025-11 發布)示範。v7 的 presigned 方法吃位置參數(bucket、key、expiry):expiry 的型別是 TimeSpan(15 分鐘寫 TimeSpan.FromMinutes(15)),而且回傳的是 Uri 不是 string(所以接住它要用 Uri,寫 string url = await ... 會直接編不過)。

// 建 client(v7:MinioClientBuilder + WithStaticCredentials)
var minio = new MinioClientBuilder("http://localhost:9000")
    .WithStaticCredentials("accessKey", "secretKey")
    .Build();

// 下載連結(GET),有效 15 分鐘
Uri url = await minio.PresignedGetObjectAsync(
    "my-bucket", "photo.jpg", TimeSpan.FromMinutes(15));

// 上傳連結(PUT),有效 10 分鐘
Uri uploadUrl = await minio.PresignedPutObjectAsync(
    "my-bucket", "uploads/photo.jpg", TimeSpan.FromMinutes(10));
版本落差很大,務必以你專案鎖定的 NuGet 版本為準:MinIO .NET SDK 在 v7(2025-11)把 presigned API 整組換掉了v7 之前的 6.x 用的是「一連串 .WithXxx(...) 串起來的 builder(鏈式呼叫)」——new PresignedGetObjectArgs().WithBucket(...).WithObject(...).WithExpiry(int 秒),expiry 是整數秒、且回傳 string;建 client 則是 new MinioClient().WithEndpoint(...).WithCredentials(...).Build()。所以從 6.x 升到 v7,這段程式要整段改寫——沒有「跨版本不變的寫法」,一律照你實際裝的版本走。

兩套差在哪?一張表看完

路線 A:AWS SDK(AWSSDK.S3)路線 B:MinIO SDK(Minio,v7)
產物標準 SigV4 URL標準 SigV4 URL(一樣)
核心 APIGetPreSignedUrlRequest + GetPreSignedURL()(回 stringPresigned{Get,Put}ObjectAsync(位置參數、回 Uri
到期怎麼填Expires = 絕對時間點DateTimeexpiry = TimeSpan
指向 MinIOServiceURL + ForcePathStyle=true本來就是連 MinIO;MinioClientBuilder 指位址
適合專案已用 AWS 生態、或要同時接真 S3專案主要就吃 MinIO、想用官方對應 SDK

MinIO 6.x 的寫法跟 v7 不同,詳見上面路線 B 的 caveat;表格與範例都以 v7 為準。)

選法很簡單:專案已經在用 AWS SDK(或哪天可能搬去真 S3),就用路線 A,一套 SDK 打天下;專案就是繞著 MinIO 轉、想用「原廠」對應工具,就用路線 B。兩者產物相容,不存在「選錯就不能用」。

怎麼用 curl 驗一條 URL(不寫程式)

拿到一條 URL 先別急著寫整合,curl 打一發最快知道它到底行不行

驗 GET(下載):直接把 URL 餵給 curl -o,能存出檔案就成功。

curl -o out.jpg "<presigned-get-url>"

驗 PUT(上傳):要用 -X PUT 並且 --upload-file 把要傳的檔案帶上(PUT 一定要有 body)。

curl -X PUT --upload-file ./photo.jpg "<presigned-put-url>"

如果這條 PUT URL 當初簽了 content-type(就是路線 A 那個 ContentType = "image/jpeg"),那你 curl必須補上對應的 header,值要一字不差:

curl -X PUT --upload-file ./photo.jpg \
     -H "Content-Type: image/jpeg" "<presigned-put-url>"

漏了、或值不一樣(例如寫成 image/png),server 就會回 SignatureDoesNotMatch這不是 bug,正是 Level 3 講的「被簽進去的東西改一個字元,簽章就對不上」——content-type 既然被簽進 SignedHeaders,你送的值就得跟簽的時候完全一致。反過來說,如果當初沒簽 content-type,就別在 curl 硬加,加了反而多送一個沒被簽的期待值、也可能對不上。

✔ 過關標準

讀完這站,你要能:

這站不碰、留給後面的

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