使用Shared Access Key上传Azure Blob报403禁止错误求助
问题描述
我正在使用Shared Access Key (SAS)将PDF文件上传至Azure Blob Storage容器,此前代码运行正常,突然开始抛出403 - Forbidden异常。响应状态消息:
Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature
我已经尝试刷新了共享访问密钥,但问题仍未解决。代码运行在.NET Framework 4.6.2控制台应用中,核心上传方法及签名生成代码如下:
原代码片段
public string AzureStorageAccountName { get; set; } public string AzureStorageAccessKey { get; set; } public string X_MS_VERSION { get { return "2017-04-17"; } } public string X_MS_CLIENT_REQUEST_ID { get { return _x_ms_client_request_id; } } public string BaseURI { get { return string.Format("https://{0}.blob.core.windows.net/", AzureStorageAccountName); } } private bool UploadBlobToStorageContainer(string filePath, string targetFolderPath, string containerName) { bool isUploaded = false; try { FileInfo fileInfo = new FileInfo(filePath); long contentLength = fileInfo.Length; long range = contentLength - 1; string method = "PUT"; string contentType = "application/pdf"; string blobName = fileInfo.Name; string blobType = "BlockBlob"; string dateString = DateTime.UtcNow.ToString("R", CultureInfo.InvariantCulture); string blobURI = BaseURI + containerName + "/" + blobName; string xmsHeader = $"x-ms-blob-type:{blobType}\nx-ms-date:{dateString}\nx-ms-version:{X_MS_VERSION}"; string resHeader = $"/{AzureStorageAccountName}/{containerName}/{blobName}"; if (!string.IsNullOrWhiteSpace(targetFolderPath)) { blobName = targetFolderPath + "/" + fileInfo.Name; } if (WebRequest.Create(blobURI) is HttpWebRequest request) { request.Method = method; request.ContentLength = contentLength; request.Headers.Add("x-ms-blob-type", blobType); request.Headers.Add("x-ms-date", dateString); request.Headers.Add("x-ms-version", X_MS_VERSION); request.Headers.Add("Authorization", GetAuthorizationHeader(method, xmsHeader, resHeader, contentType, contentLength)); using (Stream requestStream = request.GetRequestStream()) { byte[] fileContents = null; using (FileStream fs = fileInfo.OpenRead()) { fileContents = new byte[fs.Length]; fs.Read(fileContents, 0, fileContents.Length); fs.Close(); } requestStream.Write(fileContents, 0, fileContents.Length); } if (request.GetResponse() is HttpWebResponse response) { if (response.StatusCode == HttpStatusCode.Created) { isUploaded = true; } else { isUploaded = false; } } } } catch (Exception ex) { if (ex is WebException wex) { StringBuilder sb = new StringBuilder(); if (wex.Response is HttpWebResponse exr) { sb.Append("StatusCode: " + exr.StatusCode + " - "); sb.Append("Description: " + exr.StatusDescription + " - "); } sb.Append("ErrorStatus: " + wex.Status); Log.LogMessage(LogLevel.ERROR, "AzureBlobApi: UploadBlobToContainer: File upload failed. Reason: " + sb.ToString()); } Log.LogException(ex); } return isUploaded; } private string GetAuthorizationHeader(string method, string xmsHeader, string resHeader, string contentType, long contentLength) { string strToSign = $"{method}\n\n\n\n{contentLength}\n\n\n\n\n\n\n\n{xmsHeader}\n{resHeader}"; string signatureString = GetHashedString(strToSign, AzureStorageAccessKey); string authorizationHeader = string.Format( CultureInfo.InvariantCulture, "{0} {1}:{2}", "SharedKey", AzureStorageAccountName, signatureString); return authorizationHeader; } private string GetHashedString(string signingString, string accessKey) { string encString = ""; try { byte[] unicodeKey = Convert.FromBase64String(accessKey); using (HMACSHA256 hmacSha256 = new HMACSHA256(unicodeKey)) { byte[] dataToHmac = Encoding.UTF8.GetBytes(signingString); encString = Convert.ToBase64String(hmacSha256.ComputeHash(dataToHmac)); } } catch (Exception ex) { Log.LogMessage(LogLevel.ERROR, $"AzureBlobApi: GetHashedString: Exception getting hash string {ex.Message}"); } return encString; }
请求头信息
PUT 518262 x-ms-blob-type:BlockBlob x-ms-date:Sun, 30 Aug 2020 08:43:31 GMT x-ms-version:2017-04-17 /mystorage/documentcontainer/cricket/test document.pdf
问题排查与修复
我帮你找到了代码里的3个关键问题,这些是导致签名验证失败(403 Forbidden)的核心原因:
1. 请求URI与签名资源路径不匹配
你先基于原始blobName构建了blobURI和resHeader,但之后如果targetFolderPath不为空,修改了blobName却没有同步更新blobURI和resHeader。这就导致实际请求的Blob路径(包含文件夹)和签名中使用的路径(不包含文件夹)不一致,Azure验证签名时自然会失败。
2. 签名字符串缺少Content-Type字段
根据Azure SharedKey签名规则,Content-Type是必须包含在签名字符串中的字段,但你的strToSign跳过了这个位置,直接用空行代替,导致签名格式不符合要求。
3. 文件读取的潜在隐患
你使用fs.Read(fileContents, 0, fileContents.Length)读取文件内容,但Read方法不一定会一次性读取全部字节,可能导致上传的文件不完整,虽然这不是403的直接原因,但也是需要修复的问题。
修复后的代码
下面是修正后的核心代码部分:
private bool UploadBlobToStorageContainer(string filePath, string targetFolderPath, string containerName) { bool isUploaded = false; try { FileInfo fileInfo = new FileInfo(filePath); long contentLength = fileInfo.Length; string method = "PUT"; string contentType = "application/pdf"; string blobName = fileInfo.Name; string blobType = "BlockBlob"; string dateString = DateTime.UtcNow.ToString("R", CultureInfo.InvariantCulture); // 先处理blobName的文件夹拼接,再构建URI和资源路径 if (!string.IsNullOrWhiteSpace(targetFolderPath)) { blobName = $"{targetFolderPath.TrimEnd('/')}/{fileInfo.Name}"; } string blobURI = $"{BaseURI}{containerName}/{Uri.EscapeDataString(blobName)}"; string resHeader = $"/{AzureStorageAccountName}/{containerName}/{blobName}"; string xmsHeader = $"x-ms-blob-type:{blobType}\nx-ms-date:{dateString}\nx-ms-version:{X_MS_VERSION}"; if (WebRequest.Create(blobURI) is HttpWebRequest request) { request.Method = method; request.ContentLength = contentLength; request.ContentType = contentType; // 设置Content-Type请求头 request.Headers.Add("x-ms-blob-type", blobType); request.Headers.Add("x-ms-date", dateString); request.Headers.Add("x-ms-version", X_MS_VERSION); request.Headers.Add("Authorization", GetAuthorizationHeader(method, xmsHeader, resHeader, contentType, contentLength)); using (Stream requestStream = request.GetRequestStream()) { // 使用CopyTo确保完整读取文件内容 using (FileStream fs = fileInfo.OpenRead()) { fs.CopyTo(requestStream); } } using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) { isUploaded = response.StatusCode == HttpStatusCode.Created; } } } catch (Exception ex) { // 异常处理代码保持不变 if (ex is WebException wex) { StringBuilder sb = new StringBuilder(); if (wex.Response is HttpWebResponse exr) { sb.Append("StatusCode: " + exr.StatusCode + " - "); sb.Append("Description: " + exr.StatusDescription + " - "); } sb.Append("ErrorStatus: " + wex.Status); Log.LogMessage(LogLevel.ERROR, "AzureBlobApi: UploadBlobToContainer: File upload failed. Reason: " + sb.ToString()); } Log.LogException(ex); } return isUploaded; } private string GetAuthorizationHeader(string method, string xmsHeader, string resHeader, string contentType, long contentLength) { // 按照Azure签名规则,正确填充各个字段位置,包括Content-Type string strToSign = $"{method}\n" + "\n" + // Content-Encoding "\n" + // Content-Language $"{contentLength}\n" + // Content-Length "\n" + // Content-MD5 $"{contentType}\n" + // Content-Type "\n" + // Date "\n" + // If-Modified-Since "\n" + // If-Match "\n" + // If-None-Match "\n" + // If-Unmodified-Since "\n" + // Range $"{xmsHeader}\n" + $"{resHeader}"; string signatureString = GetHashedString(strToSign, AzureStorageAccessKey); string authorizationHeader = string.Format( CultureInfo.InvariantCulture, "{0} {1}:{2}", "SharedKey", AzureStorageAccountName, signatureString); return authorizationHeader; }
额外说明
- 构建
blobURI时使用Uri.EscapeDataString(blobName)处理文件名中的特殊字符(比如空格),避免URI格式错误。 - 使用
FileStream.CopyTo代替手动读取字节数组,确保文件内容完整上传。 - 确保
resHeader中的blobName和实际请求的blobName完全一致,包括文件夹路径。
内容的提问来源于stack exchange,提问作者Raghunathan Sundarrajan

