Azure Blob 存储授权
Azure Blob Storage Authorization
编辑 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 令牌是帐户 SAS,而我生成的是服务 SAS。 Service SAS 在 Azure 存储中会受到限制吗?
编辑:我尝试直接从 Azure 存储生成 SAS 令牌,这似乎确实有效。它似乎是一个帐户 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 令牌,在其他地方它在 Authorize header 中。对于上下文,我试图直接从 APIM 执行此操作,因此在下面的示例代码中,它是用其有限的 API 编写的。这只是我用来生成授权字符串的一般概念,但我在使用它时一直收到 403(我不确定是否需要从 Azure 存储端执行某些操作)。
/**
Based on https://docs.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# 还是个新手,所以如果可以做得更好,请告诉我。
我认为您不能将 SAS 令牌放入授权中 header。我找不到任何相关样本,所以我使用了
使用来自 NuGet 的 Azure.Storage.Blob C# 客户端库来执行此操作
var data = System.Text.Encoding.UTF8.GetBytes("Hello Azure Storage");
var keyCred = new StorageSharedKeyCredential(account, key);
var sasBuilder = new AccountSasBuilder()
{
Services = AccountSasServices.Blobs,
ResourceTypes = AccountSasResourceTypes.Object,
ExpiresOn = DateTimeOffset.UtcNow.AddHours(1),
Protocol = SasProtocol.Https
};
sasBuilder.SetPermissions(AccountSasPermissions.All);
var sasToken = sasBuilder.ToSasQueryParameters(keyCred).ToString();
var blobClient = new BlobServiceClient(new Uri($"https://{account}.blob.core.windows.net/?{sasToken}"), null);
var containter = blobClient.GetBlobContainerClient("test");
containter.UploadBlob("test.txt", new MemoryStream(data));
生成这样的 HTTP 请求:
PUT https://xxxxxx.blob.core.windows.net/test/test.txt?sv=2020-04-08&ss=b&srt=o&spr=https&se=2021-01-17T18%3A13%3A55Z&sp=rwdxlacuptf&sig=RI9It3O6mcmw********S%2B1r91%2Bj5zGbk%3D HTTP/1.1
Host: xxxxxx.blob.core.windows.net
x-ms-blob-type: BlockBlob
x-ms-version: 2020-04-08
If-None-Match: *
x-ms-client-request-id: c6e93312-af95-4a04-a207-2e2062b1dd26
x-ms-return-client-request-id: true
User-Agent: azsdk-net-Storage.Blobs/12.8.0 (.NET Core 3.1.10; Microsoft Windows 10.0.19042)
Request-Id: |ffa2da23-45c79d128da40651.
Content-Length: 19
Hello Azure Storage
然后直接在 WebClient 中使用 SAS 令牌,
var wc = new WebClient();
wc.Headers.Add("x-ms-blob-type: BlockBlob");
wc.UploadData($"https://{account}.blob.core.windows.net/test/test2.txt?{sasToken}", "PUT", data);
也可以,这应该是最低要求:
PUT https://xxxxx.blob.core.windows.net/test/test2.txt?sv=2020-04-08&ss=b&srt=o&spr=https&se=2021-01-17T18%3A50%3A01Z&sp=rwdxlacuptf&sig=Fj4QVfwIfjXP10G%xxxxxxxx%2FF%2FcjikizKggY%3D HTTP/1.1
Host: xxxx.blob.core.windows.net
x-ms-blob-type: BlockBlob
Connection: Keep-Alive
Content-Length: 19
Hello Azure Storage
删除 x-ms-blob-type
header 失败:
The remote server returned an error: (400) An HTTP header that's
mandatory for this request is not specified..
您可以在 GitHub 上随意浏览源代码以了解更多详细信息。
感谢 David 的帮助,确认是我的错误,我错误地转换了密钥以生成 HMAC。下面是正确的代码,注意 Base64 解码,而最初我只是得到字节数组:
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.Convert.FromBase64String(key));
var signature = System.Convert.ToBase64String(hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(string_to_sign)));
然后我可以在 APIM 策略中像这样使用它:
<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>@(string.Format("https://example-account.blob.core.windows.net/example-container/test.json?{0}", context.Variables["sas_token"]))</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-body>@((string)context.Variables["x-request-body"])</set-body>
</send-request>
Azure Storage 支持以下授权方式:
但是SAS token不能作为REST的Authorization header API。
https://docs.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的交互都是基于RESTAPI,但是像上传blob这样的操作,我不建议大家使用restapi来完成。 Azure官方提供了很多打包好的包,大家可以直接使用,比如:
https://docs.microsoft.com/en-us/dotnet/api/azure.storage.blobs?view=azure-dotnet
.Net 示例:
https://docs.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-dotnet
在上述SDK中,您可以使用sas token进行认证。
编辑 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 令牌是帐户 SAS,而我生成的是服务 SAS。 Service SAS 在 Azure 存储中会受到限制吗?
编辑:我尝试直接从 Azure 存储生成 SAS 令牌,这似乎确实有效。它似乎是一个帐户 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 令牌,在其他地方它在 Authorize header 中。对于上下文,我试图直接从 APIM 执行此操作,因此在下面的示例代码中,它是用其有限的 API 编写的。这只是我用来生成授权字符串的一般概念,但我在使用它时一直收到 403(我不确定是否需要从 Azure 存储端执行某些操作)。
/**
Based on https://docs.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# 还是个新手,所以如果可以做得更好,请告诉我。
我认为您不能将 SAS 令牌放入授权中 header。我找不到任何相关样本,所以我使用了 使用来自 NuGet 的 Azure.Storage.Blob C# 客户端库来执行此操作
var data = System.Text.Encoding.UTF8.GetBytes("Hello Azure Storage");
var keyCred = new StorageSharedKeyCredential(account, key);
var sasBuilder = new AccountSasBuilder()
{
Services = AccountSasServices.Blobs,
ResourceTypes = AccountSasResourceTypes.Object,
ExpiresOn = DateTimeOffset.UtcNow.AddHours(1),
Protocol = SasProtocol.Https
};
sasBuilder.SetPermissions(AccountSasPermissions.All);
var sasToken = sasBuilder.ToSasQueryParameters(keyCred).ToString();
var blobClient = new BlobServiceClient(new Uri($"https://{account}.blob.core.windows.net/?{sasToken}"), null);
var containter = blobClient.GetBlobContainerClient("test");
containter.UploadBlob("test.txt", new MemoryStream(data));
生成这样的 HTTP 请求:
PUT https://xxxxxx.blob.core.windows.net/test/test.txt?sv=2020-04-08&ss=b&srt=o&spr=https&se=2021-01-17T18%3A13%3A55Z&sp=rwdxlacuptf&sig=RI9It3O6mcmw********S%2B1r91%2Bj5zGbk%3D HTTP/1.1
Host: xxxxxx.blob.core.windows.net
x-ms-blob-type: BlockBlob
x-ms-version: 2020-04-08
If-None-Match: *
x-ms-client-request-id: c6e93312-af95-4a04-a207-2e2062b1dd26
x-ms-return-client-request-id: true
User-Agent: azsdk-net-Storage.Blobs/12.8.0 (.NET Core 3.1.10; Microsoft Windows 10.0.19042)
Request-Id: |ffa2da23-45c79d128da40651.
Content-Length: 19
Hello Azure Storage
然后直接在 WebClient 中使用 SAS 令牌,
var wc = new WebClient();
wc.Headers.Add("x-ms-blob-type: BlockBlob");
wc.UploadData($"https://{account}.blob.core.windows.net/test/test2.txt?{sasToken}", "PUT", data);
也可以,这应该是最低要求:
PUT https://xxxxx.blob.core.windows.net/test/test2.txt?sv=2020-04-08&ss=b&srt=o&spr=https&se=2021-01-17T18%3A50%3A01Z&sp=rwdxlacuptf&sig=Fj4QVfwIfjXP10G%xxxxxxxx%2FF%2FcjikizKggY%3D HTTP/1.1
Host: xxxx.blob.core.windows.net
x-ms-blob-type: BlockBlob
Connection: Keep-Alive
Content-Length: 19
Hello Azure Storage
删除 x-ms-blob-type
header 失败:
The remote server returned an error: (400) An HTTP header that's mandatory for this request is not specified..
您可以在 GitHub 上随意浏览源代码以了解更多详细信息。
感谢 David 的帮助,确认是我的错误,我错误地转换了密钥以生成 HMAC。下面是正确的代码,注意 Base64 解码,而最初我只是得到字节数组:
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.Convert.FromBase64String(key));
var signature = System.Convert.ToBase64String(hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(string_to_sign)));
然后我可以在 APIM 策略中像这样使用它:
<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>@(string.Format("https://example-account.blob.core.windows.net/example-container/test.json?{0}", context.Variables["sas_token"]))</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-body>@((string)context.Variables["x-request-body"])</set-body>
</send-request>
Azure Storage 支持以下授权方式:
但是SAS token不能作为REST的Authorization header API。
https://docs.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的交互都是基于RESTAPI,但是像上传blob这样的操作,我不建议大家使用restapi来完成。 Azure官方提供了很多打包好的包,大家可以直接使用,比如:
https://docs.microsoft.com/en-us/dotnet/api/azure.storage.blobs?view=azure-dotnet
.Net 示例:
https://docs.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-dotnet
在上述SDK中,您可以使用sas token进行认证。