(此文章同時發表在本人微信公眾號“dotNET每日精華文章”,歡迎右邊二維碼來關注。)
題記:為了慶祝獲得微信公眾號贊賞功能,忙里抽閑分享一下最近工作的一點心得:如何直接從瀏覽器中上傳文件到Azure Storage的Blob服務中。
為什么
如果你的Web應用程序利用了雲存儲(比如Azure Storage)來存儲用戶的資源文件(比如圖片、視頻等等)。通常的做法,是用戶訪問你的Web前端,上傳文件到你的Web后端應用,然后在后端程序中使用雲存儲的SDK把文件再轉傳到雲存儲中。架構如下圖所示:
這種模式下,雖然簡單方便。但是,由於上傳文件的過程,需要以Web后端程序作為代理,如果上傳文件巨大頻繁,會給后端程序的托管服務或托管服務器造成較大運算壓力和流量壓力。所以,還有一種模式,是讓用戶直接在瀏覽器中把文件上傳給雲存儲服務。我所熟知的雲平台(Azure ,AWS,Aliyun)都提供了類似的特性,只是實現方式或名稱上有所不同。另外,在這種方式下,為了保證安全性,一般不會直接把雲存儲的訪問Key暴露給Web前端,所以都會提供一種折中方式,讓你可以生成限時失效權限有限的共享訪問Key,把這個共享Key給到前端來獲得訪問能力。在Azure中,這個特性稱之為共享訪問簽章(Shared Access Signatures,SAS),而整個架構就變為下面這樣:
在這個架構中,你先從后端服務器獲得SAS Url,然后直接上傳文件給Azure Storage,上傳文件成功后,如果需要再把一些文件元數據傳遞給后端服務器(其實Azure的文件也可以額外保存元數據的,你自己都可以不保存元數據)。其實這種架構不僅可以運用於文件存儲服務,在Azure中還可以在前端直接訪問Azure Storage Table、Queue等服務。關於SAS模型,微軟官方的文檔《Shared Access Signatures, Part 1: Understanding the SAS Model》(http://t.cn/R4OQona)講的很清楚,上面的圖就是引自這篇文章。
當然,在很多時候,我們是需要混用這兩種模式的,在需要更多安全控制和流量可控的情況,使用代理轉傳模式;在安全可隔離流量不可控的情況下,使用直傳模式。我自己的實踐當中,也是兩種模式混用,在需要用戶上傳文件到公共存儲賬號的時候,使用代理模式,在用戶上傳文件到用戶獨有存儲賬號的時候,使用直傳模式。
由於我當前使用的雲平台是Azure,所以下面演示的代碼也是基於Azure Storage SDK的。
獲取SAS訪問地址
根據Azure的文檔《Shared Access Signatures, Part 2: Create and use a SAS with Blob storage》(http://t.cn/R4OQeBd)所述,獲取SAS其實也非常簡單。
首先實例化CloudStorageAccount、CloudBlobClient和CloudBlobContainer,如下:
//Parse the connection string and return a reference to the storage account.
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(CloudConfigurationManager.GetSetting("StorageConnectionString"));
//Create the blob client object.
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
//Get a reference to a container to use for the sample code, and create it if it does not exist.
CloudBlobContainer container = blobClient.GetContainerReference("sascontainer");
container.CreateIfNotExists();
然后創建一個臨時的策略,調用CloudBlobContainer的GetSharedAccessSignature方法來生成訪問token,如下:
//Set the expiry time and permissions for the container. //In this case no start time is specified, so the shared access signature becomes valid immediately. SharedAccessBlobPolicy sasConstraints = new SharedAccessBlobPolicy(); sasConstraints.SharedAccessExpiryTime = DateTime.UtcNow.AddHours(24); sasConstraints.Permissions = SharedAccessBlobPermissions.Write | SharedAccessBlobPermissions.List; //Generate the shared access signature on the container, setting the constraints directly on the signature. string sasContainerToken = container.GetSharedAccessSignature(sasConstraints);
所生成的token實際就是一個包含有多個策略規則的查詢字符串,然后把這個token通過Web后端的一個Api調用(當然是驗證用戶權限后)傳遞給Web前端。前端為要上傳的文件構造這樣一個Url:存儲容器的Uri+要上傳的文件名(包括所在文件夾)+SAS Token,然后把文件流HTTP PUT到這個Url就可以實現上傳。
上述代碼生成的是一個存儲容器的SAS Url,其實也可以針對一個Blob對象生成SAS Url。
另外需要注意的是,你也可以在存儲容器上設定一個固定的共享訪問策略(而非臨時設置在SAS token上),這樣你可以更加方便和系統的控制SAS的有效性。詳情可參考上面提到的文檔。
設置跨域策略
直傳文件實際上調用的是Azure Storage REST API,在最初的時候,Azure Storage是不支持跨域訪問的(CORS),在這種情況下,只有設置Storage容器的自定義域和Web應用程序的域一致。不過現在有了CORS的支持,就很簡單了,通過下面的方法可以設置CORS(:
public static async Task AddCorsRuleAsync(CloudBlobClient blobClient)
{
//First get the service properties from storage to ensure we're not adding the same CORS rule again.
var serviceProperties =await blobClient.GetServicePropertiesAsync();
var corsSettings = serviceProperties.Cors;
var corsRule = corsSettings.CorsRules.FirstOrDefault(
o => o.AllowedOrigins.Contains("http://localhost:3904"));//設置你自己的服務器地址
if (corsRule == null)
{
//Add a new rule.
corsRule = new CorsRule()
{
AllowedHeaders = new List<string> { "x-ms-*", "content-type", "accept" },
AllowedMethods = CorsHttpMethods.Put,//Since we'll only be calling Put Blob, let's just allow PUT verb
AllowedOrigins = new List<string> { "http://localhost:3904" },//This is the URL of our application.
MaxAgeInSeconds = 1 * 60 * 60,//Let the browswer cache it for an hour
};
corsSettings.CorsRules.Add(corsRule);
//Save the rule
await blobClient.SetServicePropertiesAsync(serviceProperties);
}
}
對於CORS更加詳細的解釋,可以參考Azure Storage的MSDN Blog上的這篇文章《Windows Azure Storage: Introducing CORS》和Gaurav Mantri的這篇博文《Windows Azure Storage and Cross-Origin Resource Sharing (CORS) – Lets Have Some Fun》
集成WebUploader
在獲得SAS並設置了CORS之后,最后的事情,就是如何把文件提交到SAS Url了。雖然我們可以采用Ajax或HTML5原生的方式來發送文件(如上兩篇文章中所演示的),但是使用一個現有的文件上傳組件,應該是最方便(對用戶也是如此)的方式了。
我嘗試了把Baidu WebUploader集成,實現了多圖片上傳。在集成的過程,一些注意的地方有:
- 初始化uploader對象的時候,不要設置server屬性,因為server地址需要動態獲取(要獲得SAS Url),且每個文件的SAS Url不一樣(因為文件名不一樣)
- 我是先預先獲取SAS的token,然后在uploadStart事件中為每個文件生成元數據信息,和各自的server地址
- 在uploadBeforeSend事件中,來配置Azure所需的header信息
- 在uploadSuccess事件中,把文件的元數據傳遞給后端服務器
具體的代碼可以查看我分享的代碼片段:http://git.oschina.net/ike/codes/7edc84bio2zplhunyxvkr
最后,如果這篇文章對你有用,歡迎打賞。


