Contents

使用 .NET 實作 Azure Storage SAS 上傳檔案起手勢

這邊不詳細介紹什麼是 Azure Storage,這裡簡單介紹一下其組成部分,包括 storage accountContainerblob,如下圖所示。

https://user-images.githubusercontent.com/6058558/234599213-21ae8221-bd83-4287-806e-ed464fa61fba.png

詳細資訊可以參考Blob (物件) 儲存體簡介 - Azure Storage | Microsoft Learn

由於專案時間緊迫,我只拿到了 Azure Storage 的 ConnectionString,需要在短時間內完成上傳功能。以下是使用 .NET 實作 Azure Storage SAS 上傳檔案的步驟。

心智圖

mindmap root(Azure Storage SASUrl) Container 單位上傳 Blob單位上傳 SAS上傳檔案 x-ms-blob-type: BlockBlob

專案需求

透過前端可以上傳檔案到 Azure Storage,這樣減少Server 負擔,剛好上傳東西後端也不需要做關聯,這邊這個功能符合我的需求。

大致流程

使用 .NET 實作 Azure Storage SAS 上傳檔案

  1. 建立 Storage Account

    • 登入 Azure 入口網站
    • 建立一個新的 Storage Account,並記下其 ConnectionString。
  2. 安裝 Azure.Storage.Blobs 套件
    在你的 .NET 專案中,安裝 Azure.Storage.Blobs 套件:

    1
    
    dotnet add package Azure.Storage.Blobs
    
  3. 生成 SAS Token

  4. 使用生成的 SAS URI 上傳檔案

SAS 上傳檔案方法

上面介紹了如何透過前端上傳檔案,使用 SAS 讓前端取得憑證(會是一個網址)。透過這個網址,使用者就可以進行上傳操作。

詳細的教學可以參考這篇文章:Azure Blob Storage 使用指南 – 金鑰與存取簽章篇 | 辛比誌。我參考了這篇文章後,對 SAS 的使用有了更深入的了解。

Tip
  1. 什麼是 SAS
    SAS(Shared Access Signature)是一種安全的 URL,允許你授予對 Azure Storage 資源的有限存取權限,而不需要暴露你的帳戶金鑰。

  2. 生成 SAS Token 的步驟

    • 使用 Azure Portal 或程式碼生成 SAS Token。
    • 設定 SAS Token 的有效期限和權限。
  3. 使用 SAS Token 上傳檔案

    • 使用生成的 SAS URL 進行檔案上傳。
    • 確保上傳過程中的安全性和有效性。

Container 產生 SAS 網址(不推薦使用)

這邊發現 Container SAS ,不太適合用在前端上傳,上傳權限會太大。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

        BlobContainerClient container = new("connectioningString", "containerName");
        if (!container.CanGenerateSasUri)
            throw new Exception("The container can't generate SAS URI");

        var sasBuilder = new BlobSasBuilder
        {
            BlobContainerName = container.Name,
            Resource = "c",
            ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(20)
        };

        sasBuilder.SetPermissions(BlobContainerSasPermissions.All);
        var sasUri = container.GenerateSasUri(sasBuilder);

產生 SAS Url 可以給 Client 做上傳動作。

單純透過程式做 SAS 上傳動作

下面是單純透過程式做SAS上傳動作。但通常有 ConnectionString,不太需要這樣做。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
async static Task<Uri> GetUserDelegationSasBlob(BlobClient blobClient)
{
    BlobServiceClient blobServiceClient = blobClient.GetParentBlobContainerClient().GetParentBlobServiceClient();

    // Get a user delegation key for the Blob service that's valid for 7 days.
    // You can use the key to generate any number of shared access signatures over the lifetime of the key.
    UserDelegationKey userDelegationKey = await blobServiceClient.GetUserDelegationKeyAsync(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(7));

    // Create a SAS token that's valid for 7 days.
    BlobSasBuilder sasBuilder = new BlobSasBuilder()
    {
        BlobContainerName = blobClient.BlobContainerName,
        BlobName = blobClient.Name,
        Resource = "b",
        StartsOn = DateTimeOffset.UtcNow,
        ExpiresOn = DateTimeOffset.UtcNow.AddDays(7)
    };

    // Specify read permissions for the SAS.
    sasBuilder.SetPermissions(BlobSasPermissions.Read);

    // Use the key to get the SAS token.
    string sasToken = sasBuilder.ToSasQueryParameters(userDelegationKey, blobServiceClient.AccountName).ToString();

    // Construct the full URI, including the SAS token.
    UriBuilder fullUri = new UriBuilder()
    {
        Scheme = "https",
        Host = string.Format("{0}.blob.core.windows.net", blobServiceClient.AccountName),
        Path = string.Format("{0}/{1}", blobClient.BlobContainerName, blobClient.Name),
        Query = sasToken
    };

    // Create a BlobClient object that can be used to upload a file with the SAS token
    BlobClient sasBlobClient = new BlobClient(fullUri.Uri);

    // Upload a file
    using (FileStream stream = File.OpenRead(localFilePath))
    {
        await sasBlobClient.UploadAsync(stream);
    }

    return fullUri.Uri;
}
Info

網址結構如下:
https://{azure storage ccount}.blob.core.windows.net/{blob container name}/test23.png?sv=2022-11-02&se=2023-04-24T06%3A45%3A33Z&sr=c&sp=racwdxltfi&sig=30%2By6%2F%2BGfT%2BIA2fzZKCsDTzqlArt0Fkn44rXOZ%2Baboo%3D

https://{azure storage ccount}.blob.core.windows.net/{blob container name}/test23.png

?sv=2022-11-02&se=2023-04-24T06%3A45%3A33Z&sr=c&sp=racwdxltfi&sig=30%2By6%2F%2BGfT%2BIA2fzZKCsDTzqlArt0Fkn44rXOZ%2Baboo%3D:

sv=2022-11-02: Storage 服務版本號。
se=2023-04-24T06%3A45%3A33Z: SAS Token 的到期時間(UTC 時間)。
sr=c: 資源類型,c 表示容器(container)。
sp=racwdxltfi: 許可權限,包含讀取(r)、添加(a)、建立(c)、寫入(w)、刪除(d)、列表(l)。
sig=30%2By6%2F%2BGfT%2BIA2fzZKCsDTzqlArt0Fkn44rXOZ%2Baboo%3D: SAS Token 的簽名,用於驗證 URL 的有效性和完整性。

產生 SASURL API範例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
[ApiController]
[Route("[controller]")]
public class AttachmentController : ControllerBase
{
    private readonly IConfiguration _configuration;

    public AttachmentController(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    [HttpGet("token")]
    [ProducesResponseType(StatusCodes.Status409Conflict)]
    [ProducesResponseType(typeof(AzureStorageSASResult), StatusCodes.Status200OK)]
    public IActionResult SASToken()
    {
        var azureStorageConfig = _configuration.GetSection("AppSettings:AzureStorage").Get<AzureStorageConfig>();

        BlobContainerClient container = new(azureStorageConfig.ConnectionString, azureStorageConfig.ContainerName);
        if (!container.CanGenerateSasUri)
            return Conflict("The container can't generate SAS URI");

        var sasBuilder = new BlobSasBuilder
        {
            BlobContainerName = container.Name,
            Resource = "c",
            ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(azureStorageConfig.TokenExpirationMinutes)
        };

        sasBuilder.SetPermissions(BlobContainerSasPermissions.All);
        var sasUri = container.GenerateSasUri(sasBuilder);


        return Ok(result);
    }
}
1
2
3
4
5
6
7
"AppSettings": {
  "AzureStorage": {
    "ConnectionString": "<connection string>",
    "ContainerName": "attachments",
    "TokenExpirationMinutes": 20
  }
}

Blob SAS 上傳(推薦使用)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
        BlobContainerClient container = new("connectionstring", "containerName");
        BlobServiceClient blobServiceClient = new BlobServiceClient("connectionstring");
        BlobContainerClient blobContainerClient = blobServiceClient.GetBlobContainerClient("containerName");
        BlobClient blobClient = blobContainerClient.GetBlobClient("test.png");

        Uri sasUri = GetServiceSasUriForBlob(blobClient);
		 
		 GetServiceSasUriForBlob(blobClient).Dump();


static Uri GetServiceSasUriForBlob(BlobClient blobClient,
    string storedPolicyName = null)
{
    // Check whether this BlobClient object has been authorized with Shared Key.
    if (blobClient.CanGenerateSasUri)
    {
        // Create a SAS token that's valid for one hour.
        BlobSasBuilder sasBuilder = new BlobSasBuilder()
        {
            BlobContainerName = blobClient.GetParentBlobContainerClient().Name,
			BlobName = blobClient.Name,
			Resource = "b"
		};

		if (storedPolicyName == null)
		{
			sasBuilder.ExpiresOn = DateTimeOffset.UtcNow.AddHours(1);
			sasBuilder.SetPermissions(BlobSasPermissions.Read |
				BlobSasPermissions.Write);
		}
		else
		{
			sasBuilder.Identifier = storedPolicyName;
        }

        Uri sasUri = blobClient.GenerateSasUri(sasBuilder);
        Console.WriteLine("SAS URI for blob is: {0}", sasUri);
        Console.WriteLine();

		return sasUri;
	}
	else
	{
		Console.WriteLine(@"BlobClient must be authorized with Shared Key 
                          credentials to create a service SAS.");
		return null;
	}
}

PostMan 上傳檔案

需要在Header 加上 x-ms-blob-type,這邊就可以上傳檔案。前端Web需要加上跨域設定,這邊我前台是APP,所以不需要做這件事情。

1
2
3
4
curl --location --request PUT 'https://xxxxx.blob.core.windows.net/.......' \
--header 'x-ms-blob-type: BlockBlob' \
--header 'Content-Type: image/png' \
--data '@/C:/Users/steve/OneDrive/圖片/螢幕擷取畫面/螢幕擷取畫面_20230222_100659.png'

彩蛋