c# - Azure Blob 存储授权

标签 c# azure azure-api-management azure-blob-storage

编辑2:以下是授权错误的输出:

<?xml version=\"1.0\" encoding=\"utf-8\"?>
<Error>
  <Code>AuthenticationFailed</Code>
  <Message>Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.\nRequestId:34d738a5-101e-000d-5a14-ed5956000000\nTime:2021-01-17T21:07:38.6231913Z
  </Message>
  <AuthenticationErrorDetail>Signature did not match. String to sign used was cw\n2021-01-17T19:06:42Z\n2021-01-18T21:06:42Z\n/blob/example-account/example-container/example-blob.json\n\n\nhttps\n2019-02-02\nb\n\n\n\n\n\n
  </AuthenticationErrorDetail>
</Error>

我不太明白...我更新了下面的 C# 代码以打印出带有\n 字符的 string_to_sign,它与上面输出中的 string_to_sign 完全相同。

注意:从 Azure 存储生成的有效 SAS token 是帐户 SAS,而我生成的 token 是服务 SAS。 Azure 存储中是否可以限制 Service SAS?

编辑:我尝试直接从 Azure 存储生成 SAS token ,这似乎确实有效。它似乎是一个帐户 SAS,而不是我下面尝试使用的服务 SAS。

?sv=2019-12-12&ss=b&srt=o&sp=wac&se=2021-01-18T01:15:13Z&st=2021-01-17T17:15:13Z&spr=https&sig=<signature>

我希望能够使用其 REST API 将文件上传到 Azure 存储。但是,我在获得授权时遇到了一些麻烦。我发现文档有点矛盾,在某些地方它说我可以在 URI 中包含 SAS token ,而在其他地方则它在 Authorize header 中。对于上下文,我尝试直接从 APIM 执行此操作,因此在下面的示例代码中,它是使用其有限的 API 编写的。这只是我用来生成授权字符串的一般概念,但在使用它时我不断收到 403(我不确定是否需要从 Azure 存储端执行某些操作)。

/**
 Based on https://learn.microsoft.com/en-us/rest/api/storageservices/create-service-sas
*/
using System;
using System.Collections.Generic;

namespace sas_token
{
  class Program
  {
    static void Main(string[] args)
    {
      string key = args[0];
      Console.WriteLine(generate_blob_sas_token(key));
    }

    public static string generate_blob_sas_token(string key)
    {
      const string canonicalizedResource = "canonicalizedResource";

      // NOTE: this only works for Blob type files, Tables have a different
      // structure
      // NOTE: use a List instead of Dictionary since the order of keys in
      // Dictionaries is undefined and the signature string requires a very
      // specific order
      List<KeyValuePair<string, string>> sas_token_properties = new List<KeyValuePair<string, string>>(){


    // signedPermissions, select 1..* from [racwdxltmeop], MUST be in that order
    new KeyValuePair<string, string>("sp", "cw"),

    // signedStart time, date from when the token is valid
    // NOTE: because of clock skew between services, even setting the time to
    // now may not create an immediately usable token
    new KeyValuePair<string, string>("st", DateTime.UtcNow.AddMinutes(-120).ToString("yyyy-MM-ddTHH:mm:ssZ")),

    // signedExpiry time, date until the token is valid
    new KeyValuePair<string, string>("se", DateTime.UtcNow.AddDays(1).ToString("yyyy-MM-ddTHH:mm:ssZ")),

    // canonicalizedResource, must be prefixed with /blob in recent versions
    // NOTE: this is NOT included as a query parameter, but is in the signature
    // URL = https://myaccount.blob.core.windows.net/music/intro.mp3
    // canonicalizedResource = "/blob/myaccount/music/intro.mp3"
    new KeyValuePair<string, string>(canonicalizedResource, "/blob/example-account/example-container"),

    // signedIdentifier, can be used to identify a Stored Access Policy
    new KeyValuePair<string, string>("si", ""),

    // signedIP, single or range of allowed IP addresses
    new KeyValuePair<string, string>("sip", ""),

    // signedProtocol
    // [http, https]
    new KeyValuePair<string, string>("spr", "https"),

    // signedVersion, the version of SAS used (defines which keys are
    // required/available)
    new KeyValuePair<string, string>("sv", "2019-02-02"),

    // signedResource, the type of resource the token is allowed to access
    // [b = blob, d = directory, c = container, bv, bs]
    new KeyValuePair<string, string>("sr", "b"),

    // signedSnapshotTime
    new KeyValuePair<string, string>("sst", ""),


    // the following specify how the response should be formatted

    // Cache-Control
    new KeyValuePair<string, string>("rscc", ""),

    // Content-Disposition
    new KeyValuePair<string, string>("rscd", ""),

    // Content-Encoding
    new KeyValuePair<string, string>("rsce", ""),

    // Content-Language
    new KeyValuePair<string, string>("rscl", ""),

     // Content-Type
    new KeyValuePair<string, string>("rsct", "")
};

      // the format is a very specific text string, where values are delimited by new
      // lines, and the order of the properties in the string is important!
      List<string> values = new List<string>();
      foreach (KeyValuePair<string, string> entry in sas_token_properties)
      {
        values.Add(entry.Value);
      }
      string string_to_sign = string.Join("\n", new List<string>(values));
      Console.WriteLine(string_to_sign.Replace("\n", "\\n"));
      System.Security.Cryptography.HMACSHA256 hmac = new System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(key));
      var signature = System.Convert.ToBase64String(hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(string_to_sign)));

      // create the query parameters of any set values + the signature
      // NOTE: all properties that contribute to the signature must be added
      // as query params EXCEPT canonicalizedResource
      List<string> parameters = new List<string>();

      foreach (KeyValuePair<string, string> entry in sas_token_properties)
      {
        if (!string.IsNullOrEmpty(entry.Value) && entry.Key != canonicalizedResource)
        {
          parameters.Add(entry.Key + "=" + System.Net.WebUtility.UrlEncode(entry.Value));
        }
      }

      parameters.Add("sig=" + System.Net.WebUtility.UrlEncode(signature));

      string sas_token_querystring = string.Join("&", parameters);

      return sas_token_querystring;
    }
  }
}

我使用以下(简化的)APIM 策略中的输出(我将“sas_token”变量设置为函数的输出来测试流程):

<set-variable name="x-request-body" value="@(context.Request.Body.As<string>())" />
<send-request mode="new" response-variable-name="tokenstate" timeout="20" ignore-error="true">
   <set-url>@("https://example-account.blob.core.windows.net/example-container/test.json")</set-url>
   <set-method>PUT</set-method>
   <set-header name="x-ms-date" exists-action="override">
     <value>@(DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"))</value>
   </set-header>
   <set-header name="x-ms-version" exists-action="override">
     <value>2019-02-02</value>
   </set-header>
   <set-header name="x-ms-blob-type" exists-action="override">
     <value>BlockBlob</value>
   </set-header>
   <set-header name="Authorization" exists-action="override">
     <value>@("SharedAccessSignature " + (string)context.Variables["sas_token"])</value>
   </set-header>
   <set-body>@((string)context.Variables["x-request-body"])</set-body>
</send-request>

为了完整起见,以下是我使用 {"hello": "then"} 跟踪测试请求时 APIM 的结果:

{
    "message": "Request is being forwarded to the backend service. Timeout set to 20 seconds",
    "request": {
        "method": "PUT",
        "url": "https://example-account.blob.core.windows.net/example-container/test.json",
        "headers": [
            {
                "name": "Host",
                "value": "example-account.blob.core.windows.net"
            },
            {
                "name": "Content-Length",
                "value": 17
            },
            {
                "name": "x-ms-date",
                "value": "2021-01-17T16:53:28Z"
            },
            {
                "name": "x-ms-version",
                "value": "2019-02-02"
            },
            {
                "name": "x-ms-blob-type",
                "value": "BlockBlob"
            },
            {
                "name": "Authorization",
                "value": "SharedAccessSignature sp=cw&st=2021-01-17T13%3A42%3A02Z&se=2021-01-18T15%3A42%3A02Z&spr=https&sv=2019-02-02&sr=b&sig=<signature>"
            },
            {
                "name": "X-Forwarded-For",
                "value": "205.193.94.40"
            }
        ]
    }
}
send-request (92.315 ms)
{
    "response": {
        "status": {
            "code": 403,
            "reason": "Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature."
        },
        "headers": [
            {
                "name": "x-ms-request-id",
                "value": "185d86f5-601e-0038-5cf1-ec3542000000"
            },
            {
                "name": "Content-Length",
                "value": "321"
            },
            {
                "name": "Content-Type",
                "value": "application/xml"
            },
            {
                "name": "Date",
                "value": "Sun, 17 Jan 2021 16:53:28 GMT"
            },
            {
                "name": "Server",
                "value": "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0"
            }
        ]
    }
}

另外,对于 C# 来说还是新手,所以如果可以做得更好,请告诉我。

最佳答案

Azure 存储支持以下授权方法:

enter image description here

但是SAS token不能作为REST API的Authorization header。

https://learn.microsoft.com/en-us/rest/api/storageservices/authorize-requests-to-azure-storage

我封装了几种认证方法:

using Azure.Storage;
using Azure.Storage.Sas;
using Microsoft.Azure.Services.AppAuthentication;
using System;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;

namespace ConsoleApp31
{
    class Program
    {
        static void Main(string[] args)
        {

            string storageKey = "xxxxxx";
            string storageAccount = "yourstorageaccountname";
            string containerName = "test";
            string blobName = "test.txt";
            string mimeType = "text/plain";

            string test = "This is a test of bowman.";
            byte[] byteArray = Encoding.UTF8.GetBytes(test);
            MemoryStream stream = new MemoryStream(byteArray);

            UseRestApiToUpload(storageKey,storageAccount,containerName,blobName,stream,mimeType);

            Console.WriteLine("*******");
            Console.ReadLine();
        }

        //Upload blob with REST API
        static void UseRestApiToUpload(string storageKey, string storageAccount, string containerName, string blobName, Stream stream, string mimeType)
        {
            string method = "PUT";
            long contentlength = stream.Length;

            string requestUri = $"https://{storageAccount}.blob.core.windows.net/{containerName}/{blobName}";

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(requestUri);

            string utcnow = DateTime.UtcNow.ToString("R");

            var memoryStream = new MemoryStream();
            stream.CopyTo(memoryStream);
            var content = memoryStream.ToArray();

            request.Method = method;

            request.Headers.Add("Content-Type", mimeType);
            request.Headers.Add("x-ms-version", "2019-12-12");
            request.Headers.Add("x-ms-date", utcnow);
            request.Headers.Add("x-ms-blob-type", "BlockBlob");
            request.Headers.Add("Content-Length", contentlength.ToString());

            //Use SharedKey to authorize.
            request.Headers.Add("Authorization", AuthorizationHeaderWithSharedKey(method, utcnow, request, storageAccount, storageKey, containerName, blobName));

            //Can not use SAS token in REST API header to authorize. 

            //Use Bearer token to authorize.
            //request.Headers.Add("Authorization",AuthorizationHeaderWithAzureActiveDirectory());



            using (Stream requestStream = request.GetRequestStream())
            {
                requestStream.Write(content, 0, (int)contentlength);
            }

            using (HttpWebResponse resp = (HttpWebResponse)request.GetResponse())
            {
            }

        }

        //Use shared key to authorize.
        public static string AuthorizationHeaderWithSharedKey(string method, string now, HttpWebRequest request, string storageAccount, string storageKey, string containerName, string blobName)
        {

            string headerResource = $"x-ms-blob-type:BlockBlob\nx-ms-date:{now}\nx-ms-version:2019-12-12";
            string urlResource = $"/{storageAccount}/{containerName}/{blobName}";
            string stringToSign = $"{method}\n\n\n{request.ContentLength}\n\n{request.ContentType}\n\n\n\n\n\n\n{headerResource}\n{urlResource}";

            HMACSHA256 hmac = new HMACSHA256(Convert.FromBase64String(storageKey));
            string signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));

            String SharedKey = String.Format("{0} {1}:{2}", "SharedKey", storageAccount, signature);
            return SharedKey;
        }


        //Use Shared access signature(SAS) to authorize.
        public static string AuthorizationHeaderWithSharedAccessSignature(string storageAccount, string storageKey)
        {
            // Create a SAS token that's valid for one hour.
            AccountSasBuilder sasBuilder = new AccountSasBuilder()
            {
                Services = AccountSasServices.Blobs | AccountSasServices.Files,
                ResourceTypes = AccountSasResourceTypes.Service,
                ExpiresOn = DateTimeOffset.UtcNow.AddHours(1),
                Protocol = SasProtocol.Https
            };

            sasBuilder.SetPermissions(AccountSasPermissions.Read |
                AccountSasPermissions.Write);

            // Use the key to get the SAS token.
            StorageSharedKeyCredential key = new StorageSharedKeyCredential(storageAccount, storageKey);
            string sasToken = sasBuilder.ToSasQueryParameters(key).ToString();

            Console.WriteLine("SAS token for the storage account is: {0}", sasToken);
            Console.WriteLine();

            return sasToken;
        }


        //Use Azure Active Directory(Bearer token) to authorize.
        public static string AuthorizationHeaderWithAzureActiveDirectory()
        {
            AzureServiceTokenProvider azureServiceTokenProvider = new AzureServiceTokenProvider();
            string bearertoken = azureServiceTokenProvider.GetAccessTokenAsync("https://storage.azure.com/").Result;
            return "Bearer " + bearertoken;
        }
    }
}

虽然很多软件包和azure之间的交互都是基于REST API,但是对于上传blob之类的操作,我不建议您使用rest api来完成。 Azure官方提供了很多打包好的包可以直接使用,比如:

https://learn.microsoft.com/en-us/dotnet/api/azure.storage.blobs?view=azure-dotnet

.Net 的示例:

https://learn.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-dotnet

在上述SDK中,您可以使用sas token进行身份验证。

关于c# - Azure Blob 存储授权,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65763275/

相关文章:

c# - 使用 Process.Start : Firefox not starting when you set Usename and Password 启动 Firefox

c# - 将结构转换为对象以进行空值比较不会导致装箱吗?

Azure 云服务和 Amazon Cloudfront 互操作性

python - Azure Bing 图像搜索客户端抛出找不到资源的错误

azure - 查询订阅配额

c# - 检查daylist是否连续几天

c# - 从 UITableView 中删除一个部分。 NSIndexSet 参数?

用于列出 Key Vault 中所有 secret 的 Azure Synapse Spark 池命令

用于 JWT 验证的 JWK 的 Azure API 管理策略缓存

azure - 在Azure API管理中,是否可以有一个允许斜杠(例如路径)的模板参数?