【问题标题】:REST request on an Azure storage account using an access key results in HTTP 403 Forbidden使用访问密钥对 Azure 存储帐户进行 REST 请求会导致 HTTP 403 Forbidden
【发布时间】:2020-11-14 08:36:21
【问题描述】:

我有一个没有公共 blob 访问权限的 Azure 存储帐户。我可以使用存储帐户访问密钥之一通过 (.NET) API 访问 blob、表和查询。对于 REST,我在 https://docs.microsoft.com/en-us/azure/storage/common/storage-rest-api-auth 上尝试了 Microsoft 演示应用程序,当然还有我的存储帐户名称和存储帐户访问密钥之一。此演示应用程序仅列出 blob 容器。尝试连接时会导致 HTTP 403(禁止)。

我找不到原因。存储帐户访问密钥是否正确使用(由于某种原因我无法创建共享访问签名来尝试它们)?想法受到赞赏。

这是完整代码(请注意,我将存储帐户名称和访问密钥替换为“xxx”):

using System;
using System.Globalization;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;

internal static class Program
{
    static string StorageAccountName = "xxx";
    static string StorageAccountKey = "xxx";
    
    private static void Main()
    {
        // List the containers in a storage account.
        ListContainersAsyncREST(StorageAccountName, StorageAccountKey, CancellationToken.None).GetAwaiter().GetResult();

        Console.WriteLine("Press any key to continue.");
        Console.ReadLine();
    }

    /// <summary>
    /// This is the method to call the REST API to retrieve a list of
    /// containers in the specific storage account.
    /// This will call CreateRESTRequest to create the request, 
    /// then check the returned status code. If it's OK (200), it will 
    /// parse the response and show the list of containers found.
    /// </summary>
    private static async Task ListContainersAsyncREST(string storageAccountName, string storageAccountKey, CancellationToken cancellationToken)
    {

        // Construct the URI. This will look like this:
        //   https://myaccount.blob.core.windows.net/resource
        String uri = string.Format("http://{0}.blob.core.windows.net?comp=list", storageAccountName);

        // Set this to whatever payload you desire. Ours is null because 
        //   we're not passing anything in.
        Byte[] requestPayload = null;

        //Instantiate the request message with a null payload.
        using (var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, uri)
        { Content = (requestPayload == null) ? null : new ByteArrayContent(requestPayload) })
        {

            // Add the request headers for x-ms-date and x-ms-version.
            DateTime now = DateTime.UtcNow;
            httpRequestMessage.Headers.Add("x-ms-date", now.ToString("R", CultureInfo.InvariantCulture));
            httpRequestMessage.Headers.Add("x-ms-version", "2017-04-17");
            // If you need any additional headers, add them here before creating
            //   the authorization header. 

            // Add the authorization header.
            httpRequestMessage.Headers.Authorization = AzureStorageAuthenticationHelper.GetAuthorizationHeader(
               storageAccountName, storageAccountKey, now, httpRequestMessage);

            // Send the request.
            using (HttpResponseMessage httpResponseMessage = await new HttpClient().SendAsync(httpRequestMessage, cancellationToken))
            {
                // If successful (status code = 200), 
                //   parse the XML response for the container names.
                if (httpResponseMessage.StatusCode == HttpStatusCode.OK)
                {
                    String xmlString = await httpResponseMessage.Content.ReadAsStringAsync();
                    XElement x = XElement.Parse(xmlString);
                    foreach (XElement container in x.Element("Containers").Elements("Container"))
                    {
                        Console.WriteLine("Container name = {0}", container.Element("Name").Value);
                    }
                }
            }
        }
    }
}

【问题讨论】:

  • 请看看这是否回答了您的问题:stackoverflow.com/questions/60211422/…。本质上这是示例中的错误。
  • @Gaurav Mantri-AIS:感谢您的提示,但不幸的是它没有帮助。但它为我指明了尝试自己构建授权标头的方向
  • @Gaurav Mantri-AIS:更正:解决方案有帮助(只是在 Microsoft 演示中没有)。在我自己的演示中,现在可以访问了

标签: azure azure-storage


【解决方案1】:

我认为您的存储帐户设置为仅允许 HTTPS,这意味着您需要将 uri 从 HTTP 更改为 HTTPS。

改变这个:

String uri = string.Format("http://{0}.blob.core.windows.net?comp=list", storageAccountName);

到这里:

String uri = string.Format("https://{0}.blob.core.windows.net?comp=list", storageAccountName);

使用您的代码这样做对我有用,我只通过从门户复制粘贴来输入存储帐户名称和访问密钥。

【讨论】:

  • 感谢您找到这个,但不幸的是,即使使用 https,我也得到 HTTP 403
  • @JürgenBayer 哦。唔。再次复制您的密钥并确保存储帐户名称为小写字母。它应该适用于您的代码。也许您已经激活了 VNET 集成从而阻止了流量?
  • 我复制并粘贴了存储帐户名称和访问密钥,再次尝试以 100% 确定,VNET 集成我没有设置,甚至不知道在哪里可以找到它。 Azure 帐户非常新鲜,只有一个存储帐户、一个网络安全组(用于 VM 测试)和一个资源组。存储帐户的访问权限设置为所有网络。
  • @JürgenBayer 既然我们知道代码有效,我建议您创建一个通用 V2 类型的新存储帐户,在其中创建一个容器,看看它是否适合您。我只是这样做了,而且效果很好。
  • 另外请检查对象上的“ReasonPrase”的内容:httpResponseMessage
【解决方案2】:

基于 Microsoft 示例代码、我的问题上的 cmets 以及 https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key 上的文档,我开发了一个同时也适用于表请求的解决方案。对于其他在 REST 存储请求上苦苦挣扎的人,这里就是。请注意,授权标头用于 Blob、队列和表服务的 2009-09-19 版本及更高版本以及文件服务的 2014-02-14 版本及更高版本的 SharedKey 授权。

/// <summary>
/// Creates the authorization headers needed for Azure Storage REST calls.
/// </summary>
/// <remarks>
/// This class is bases on the Microsoft sample code on 
/// https://github.com/Azure-Samples/storage-dotnet-rest-api-with-auth
/// </remarks>
internal static class AzureStorageAuthenticationHelper
{
    /// <summary>
    /// Creates a SharedKey authorization header for blob, query and file requests according to 
    /// https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key#blob-queue-and-file-services-shared-key-authorization.
    /// </summary>
    /// <param name="storageAccountName">The name of the storage account to use.</param>
    /// <param name="storageAccountKey">The access key for the storage account to be used.</param>
    /// <param name="now">Date/Time for now. Note that a request must not be older than 15 minutes. 
    /// Otherwise a HTTP 403 (Forbidden) results.</param>
    /// <param name="httpRequestMessage">The HttpWebRequest that needs an authorization header.</param>
    /// <param name="ifMatch">Provide an eTag, and a blob is only modified, if the current eTag matches. 
    /// This ensures that changes of others are not overwritten (provided, they add a eTag too).</param>
    /// <param name="md5">If not null the passed md5 ic checked if it matches the blob's md5. If the md5 does
    /// not match, the query will not return a value.</param>
    internal static AuthenticationHeaderValue GetAuthorizationHeaderForBlobAndQueueAndFile(string storageAccountName, string storageAccountKey,
       DateTime now, HttpRequestMessage httpRequestMessage, string ifMatch = "", string md5 = "")
    {
        // This is the raw representation of the message signature
        HttpMethod method = httpRequestMessage.Method;

        var headerContentLength = method == HttpMethod.Get || method == HttpMethod.Head
            ? String.Empty
            : httpRequestMessage.Content.Headers.ContentLength.ToString();
        var cannonicalHeaders = GetCanonicalizedHeaders(httpRequestMessage);
        var cannnonicalResource = GetCanonicalizedResourceForBlobAndQueue(httpRequestMessage.RequestUri, storageAccountName);

        // Create a signature according to https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key#blob-queue-and-file-services-shared-key-authorization
        // for the 2009-09-19 version 
        var signatureStringToSign =
            method + "\n" +                 // VERB
            "\n" +                          // Content-Encoding 
            "\n" +                          // Content-Language
            headerContentLength + "\n" +    // Content-Length
            md5 + "\n" +                    // Content-MD5 
            "\n" +                          // Content-Type
            "\n" +                          // Date
            "\n" +                          // If-Modified-Since
            ifMatch + "\n" +                // If-Match
            "\n" +                          // If-None-Match
            "\n" +                          // If-Unmodified-Since
            "\n" +                          // Range
            cannonicalHeaders + cannnonicalResource;
        var storageAccountKeyMessageAuthenticationCode = new HMACSHA256(Convert.FromBase64String(storageAccountKey));
        var signature = Convert.ToBase64String(storageAccountKeyMessageAuthenticationCode.ComputeHash(
            Encoding.UTF8.GetBytes(signatureStringToSign)));

        // Create the actual header that will be added to the list of request headers
        var authenticationHeaderValue = new AuthenticationHeaderValue("SharedKey", storageAccountName + ":" + signature);

        return authenticationHeaderValue;
    }

    /// <summary>
    /// Creates a SharedKey authorization header for table requests according to 
    /// https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key#table-service-shared-key-authorization
    /// </summary>
    /// <param name="storageAccountName">The name of the storage account to use.</param>
    /// <param name="storageAccountKey">The access key for the storage account to be used.</param>
    /// <param name="now">Date/Time for now. Note that a request must not be older than 15 minutes. 
    /// Otherwise a HTTP 403 (Forbidden) results.</param>
    /// <param name="httpRequestMessage">The HttpWebRequest that needs an authorization header.</param>
    /// <param name="ifMatch">Provide an eTag, and a blob is only modified, if the current eTag matches. 
    /// This ensures that changes of others are not overwritten (provided, they add a eTag too).</param>
    /// <param name="md5">If not null the passed md5 ic checked if it matches the blob's md5. If the md5 does
    /// not match, the query will not return a value.</param>
    internal static AuthenticationHeaderValue GetAuthorizationHeaderForTable(string storageAccountName, string storageAccountKey,
       DateTime now, HttpRequestMessage httpRequestMessage, string md5 = "")
    {
        // This is the raw representation of the message signature
        HttpMethod method = httpRequestMessage.Method;

        var headerContentLength = method == HttpMethod.Get || method == HttpMethod.Head
            ? String.Empty
            : httpRequestMessage.Content.Headers.ContentLength.ToString();
        var date = now.ToString("R", CultureInfo.InvariantCulture);
        var cannnonicalResource = GetCanonicalizedResourceForTable(httpRequestMessage.RequestUri, storageAccountName);

        // Create a signature according to https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key#table-service-shared-key-authorization
        // for the 2009-09-19 version. Note that the date must be specified here and be the same as in the x-ms-date header.        
        var signatureStringToSign =
            method + "\n" +     // VERB
            md5 + "\n" +        // Content-MD5
            "\n" +              // Content-Type 
            date + "\n" +       // Date
            cannnonicalResource;
        var storageAccountKeyMessageAuthenticationCode = new HMACSHA256(Convert.FromBase64String(storageAccountKey));
        var signature = Convert.ToBase64String(storageAccountKeyMessageAuthenticationCode.ComputeHash(
            Encoding.UTF8.GetBytes(signatureStringToSign)));

        // Create the actual header that will be added to the list of request headers
        var authenticationHeaderValue = new AuthenticationHeaderValue("SharedKey", storageAccountName + ":" + signature);

        return authenticationHeaderValue;
    }

    /// <summary>
    /// Gets a canonical string for the x-ms headers of a HTTP request according to
    /// https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key#constructing-the-canonicalized-headers-string.
    /// </summary>
    /// <remarks>
    /// A cannnical string is a string "in the right format".
    /// </remarks>
    /// </summary>
    /// <param name="httpRequestMessage">The request that will be made to the storage service</param>
    /// <returns>Error message in case of an exception</returns>
    private static string GetCanonicalizedHeaders(HttpRequestMessage httpRequestMessage)
    {
        // Get the x-ms headers with lowercase key and value
        var microsoftHeaders =
            from header in httpRequestMessage.Headers
            where header.Key.StartsWith("x-ms-", StringComparison.OrdinalIgnoreCase)
            orderby header.Key
            select new { Key = header.Key.ToLowerInvariant(), header.Value };


        // Create the string in the right format; this is what makes the headers "canonicalized",
        // meaning to put it in a standard format. See http://en.wikipedia.org/wiki/Canonicalization
        var resultBuilder = new StringBuilder();
        foreach (var microsoftHeader in microsoftHeaders)
        {
            var headerBuilder = new StringBuilder(microsoftHeader.Key);
            var separator = ':';

            // Get the value for each header, remove \r\n, and append the value separated by the current separator
            foreach (string headerValue in microsoftHeader.Value)
            {
                var trimmedValue = headerValue.TrimStart().Replace("\r\n", String.Empty);
                headerBuilder.Append(separator).Append(trimmedValue);

                // After the first value, set the separator to a comma
                separator = ',';
            }

            // Append the header
            resultBuilder.Append(headerBuilder.ToString()).Append("\n");
        }

        return resultBuilder.ToString();
    }

    /// <summary>
    /// Creates a canonical string representing the storage service resource targeted by the request according to
    /// https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key#shared-key-format-for-2009-09-19-and-later
    /// for blob and queue services.
    /// </summary>
    /// <remarks>
    /// A cannnical string is a string "in the right format".
    /// </remarks>
    /// <param name="address">The URI of the storage service.</param>
    /// <param name="accountName">The storage account name.</param>
    /// <returns>String representing the canonicalized resource.</returns>
    private static string GetCanonicalizedResourceForBlobAndQueueX(Uri address, string storageAccountName)
    {
        // The absolute path is "/" because for we're getting a list of containers.
        var resultBuilder = new StringBuilder("/").Append(storageAccountName).Append(address.AbsolutePath);

        // Address.Query is the resource, such as "?comp=list".
        // This ends up with a NameValueCollection with 1 entry having key=comp, value=list.
        // It will have more entries if you have more query parameters.
        var queryValues = HttpUtility.ParseQueryString(address.Query);

        foreach (var item in queryValues.AllKeys.OrderBy(key => key))
        {
            resultBuilder.Append('\n').Append(item.ToLower()).Append(':').Append(queryValues[item]);
        }

        return resultBuilder.ToString();
    }

    /// <summary>
    /// Creates a canonical string representing the storage service resource targeted by the request according to
    /// https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key#shared-key-format-for-2009-09-19-and-later
    /// for blob and queue services.
    /// </summary>
    /// <remarks>
    /// A cannnical string is a string "in the right format".
    /// </remarks>
    /// <param name="address">The URI of the storage service.</param>
    /// <param name="accountName">The storage account name.</param>
    /// <returns>String representing the canonicalized resource.</returns>
    private static string GetCanonicalizedResourceForBlobAndQueue(Uri address, string storageAccountName)
    {
        // 1. Beginning with an empty string (""), append a forward slash (/), followed by the name 
        // of the account that owns the resource being accessed.
        var resultBuilder = new StringBuilder($"/{storageAccountName}");

        // 2. Append the resource's encoded URI path, without any query parameters.
        resultBuilder.Append(address.AbsolutePath);

        // 3. Retrieve all query parameters on the resource URI, including the comp parameter if it exists.
        var queryValues = HttpUtility.ParseQueryString(address.Query);

        // 4. Convert all parameter names to lowercase.
        // 5. Sort the query parameters lexicographically by parameter name, in ascending order.
        // 6. URL-decode each query parameter name and value.
        // 7. Include a new-line character (\n) before each name-value pair.
        // 8. Append each query parameter name and value to the string in the following format, 
        //    making sure to include the colon (:) between the name and the value: parameter - name:parameter - value
        // 9. If a query parameter has more than one value, sort all values lexicographically, then include them in a comma-separated list:
        //    parameter - name:parameter - value - 1,parameter - value - 2,parameter - value - n
        foreach (var item in queryValues.AllKeys.OrderBy(key => key))
        {
            resultBuilder.Append('\n').Append(item.ToLower()).Append(':').Append(queryValues[item]);
        }

        return resultBuilder.ToString();
    }

    /// <summary>
    /// Creates a canonical string representing the storage service resource targeted by the request according to
    /// https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key#shared-key-lite-and-table-service-format-for-2009-09-19-and-later
    /// for table services.
    /// </summary>
    /// <remarks>
    /// A cannnical string is a string "in the right format".
    /// </remarks>
    /// <param name="address">The URI of the storage service.</param>
    /// <param name="accountName">The storage account name.</param>
    /// <returns>String representing the canonicalized resource.</returns>
    private static string GetCanonicalizedResourceForTable(Uri address, string storageAccountName)
    {
        // 1. Beginning with an empty string (""), append a forward slash (/), followed by the name 
        // of the account that owns the resource being accessed.
        var resultBuilder = new StringBuilder($"/{storageAccountName}");

        // 2a. Append the resource's encoded URI path. 
        resultBuilder.Append(address.AbsolutePath);

        // 2b. If the request URI addresses a component of the resource, append the appropriate query string. 
        // The query string should include the question mark and the comp parameter 
        // (for example, ?comp=metadata). No other parameters should be included on the query string.
        var queryValues = HttpUtility.ParseQueryString(address.Query);
        if (queryValues.AllKeys.Contains("comp"))
        {
            resultBuilder.Append($"?{queryValues["comp"]}");
        }

        return resultBuilder.ToString();
    }
}

【讨论】:

    猜你喜欢
    • 2020-08-22
    • 2019-07-28
    • 2020-11-09
    • 2019-11-08
    • 2021-04-06
    • 1970-01-01
    • 2021-05-12
    • 2021-12-21
    • 2011-06-21
    相关资源
    最近更新 更多