如何生成Azure SAS Token
在Azure PaaS服务密钥的安全性文章中,提到过客户端实际上发送的是Token,而不是密钥。那么Token是该如何生成呢?
Azure相应服务的SDK其实都提供了或者内置了生成Token的方法,可以直接调用,但是如果是想通过REST API的方式访问,而不像依赖于SDK,那么就需要自行生成Token了。其实Token本质上就是一个有一定规则的字符串,所以实现起来也不难。
一般情况下,Token的格式是这样子的
SharedAccessSignature sig={signature-string}&se={expiry}&skn={policyName}&sr={URL-encoded-resourceURI}
其中各个参数的期望的值是:
- signature-string:基于密钥使用HMAC-SHA256算法加密字符串“{URL-encoded-resourceURI} + "\n" + expiry”,然后转换成base64并URL编码
- expiry:从1970-01-01 00:00:00算起到你期望过期时间的总秒数,并转换UTF8字符串
- policyName:密钥对应的共享访问规则名称
- URL-encoded-resourceURI:URL编码的小写的目标访问资源URL字符串
有一个比较坑的地方要注意下,Service Bus和IoT Hub令牌生成的格式虽然看上去一样,但在生成signature-string时有细微的区别:
- Service Bus:直接获取密钥的byte数组
var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key))
- IoT Hub:按Base64字符串解码
var hmac = new HMACSHA256(Convert.FromBase64String(key));
完整示例代码如下:
using System; using System.Web; using System.Text; using System.Security.Cryptography; using System.Globalization; static string createToken(string resourceUri, string keyName, string key) { var encodedResourceUri = HttpUtility.UrlEncode(resourceUri); var sinceEpoch = DateTime.UtcNow - new DateTime(1970, 1, 1); var week = 60 * 60 * 24 * 7; var expiry = Convert.ToString((int)sinceEpoch.TotalSeconds + week); var stringToSign = encodedResourceUri + "\n" + expiry; var stringToSignInBytes = Encoding.UTF8.GetBytes(stringToSign); // For Service Bus var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key)); // For IoT Hub // var hmac = new HMACSHA256(Convert.FromBase64String(key)); var signature = Convert.ToBase64String(hmac.ComputeHash(stringToSignInBytes)); var encodedSignature = HttpUtility.UrlEncode(signature); var sasToken = String.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}", encodedResourceUri, encodedSignature, expiry, keyName); return sasToken; }
而Storage则要复杂得多,而且有两种格式。一种是用做Authorization头部的SharedKey格式,另一种则是可放置在URL中的SAS格式
SharedKey格式
格式为
SharedKey {AccountName}:{Signature}
而且Table和其他几种存储服务的签名字符串格式还不一样。
Blob,Queue和File
StringToSign = VERB + "\n" +
Content-Encoding + "\n" +
Content-Language + "\n" +
Content-Length + "\n" +
Content-MD5 + "\n" +
Content-Type + "\n" +
Date + "\n" +
If-Modified-Since + "\n" +
If-Match + "\n" +
If-None-Match + "\n" +
If-Unmodified-Since + "\n" +
Range + "\n" +
CanonicalizedHeaders +
CanonicalizedResource;
Table
StringToSign = VERB + "\n" +
Content-MD5 + "\n" +
Content-Type + "\n" +
Date + "\n" +
CanonicalizedResource;
具体示例代码如下
构建标准化的头部字符串
public string GetCanonicalizedHeaders(HttpWebRequest request) { ArrayList headerNameList = new ArrayList(); StringBuilder sb = new StringBuilder(); foreach (string headerName in request.Headers.Keys) { if (headerName.ToLowerInvariant().StartsWith("x-ms-", StringComparison.Ordinal)) { headerNameList.Add(headerName.ToLowerInvariant()); } } headerNameList.Sort(); foreach (string headerName in headerNameList) { StringBuilder builder = new StringBuilder(headerName); string separator = ":"; foreach (string headerValue in GetHeaderValues(request.Headers, headerName)) { string trimmedValue = headerValue.Replace("\r\n", String.Empty); builder.Append(separator); builder.Append(trimmedValue); separator = ","; } sb.Append(builder.ToString()); sb.Append("\n"); } return sb.ToString(); } private ArrayList GetHeaderValues(NameValueCollection headers, string headerName) { ArrayList list = new ArrayList(); string[] values = headers.GetValues(headerName); if (values != null) { foreach (string str in values) { list.Add(str.TrimStart(null)); } } return list; }
构建标准化的资源字符串
public string GetCanonicalizedResource(Uri address, string accountName, bool isTableStorage = false) { StringBuilder canonicalizedResourceStrBuilder = new StringBuilder(); StringBuilder builder = new StringBuilder("/"); builder.Append(accountName); builder.Append(address.AbsolutePath); canonicalizedResourceStrBuilder.Append(builder.ToString()); NameValueCollection values2 = new NameValueCollection(); if (!isTableStorage) { NameValueCollection values = HttpUtility.ParseQueryString(address.Query); foreach (string str2 in values.Keys) { ArrayList list = new ArrayList(values.GetValues(str2)); list.Sort(); StringBuilder builder2 = new StringBuilder(); foreach (object obj2 in list) { if (builder2.Length > 0) { builder2.Append(","); } builder2.Append(obj2.ToString()); } values2.Add((str2 == null) ? str2 : str2.ToLowerInvariant(), builder2.ToString()); } } ArrayList list2 = new ArrayList(values2.AllKeys); list2.Sort(); foreach (string str3 in list2) { StringBuilder builder3 = new StringBuilder(string.Empty); builder3.Append(str3); builder3.Append(":"); builder3.Append(values2[str3]); canonicalizedResourceStrBuilder.Append("\n"); canonicalizedResourceStrBuilder.Append(builder3.ToString()); } return canonicalizedResourceStrBuilder.ToString(); }
获取SharedKey类型认证Token
这里要注意的是,当发送的请求中使用了哪些头部,那么这里也有设置相应头部的值。比如一个putblob请求,包含了MD5头部值,那么这里生成token,也需要传入相应的MD5值,否则会验证失败。
public string GetAuthorizationToken( string storageAccountName, string requestUri, string method, DateTime date, string contentEncoding = "", string contentLanguage = "", int contentLength = 0, string contentMd5 = "", string contentType = "", string ifModifiedSince = "", string ifMatch = "", string ifNoneMatch = "", string ifUnmodifiedSince = "", string range = "", bool isTableStorage = false) { HttpWebRequest request = HttpWebRequest.Create(requestUri) as HttpWebRequest; request.Headers.Add("x-ms-date", date.ToString("R", System.Globalization.CultureInfo.InvariantCulture)); request.Headers.Add("x-ms-version", "2017-04-17"); string messageSignature; if (isTableStorage) { messageSignature = String.Format("{0}\n{1}\n{2}\n{3}\n{4}", method, contentMd5, contentType, date.ToString("R", System.Globalization.CultureInfo.InvariantCulture), GetCanonicalizedResource(request.RequestUri, storageAccountName) ); } else { string canonicalizedHeaders = GetCanonicalizedHeaders(request); string canonicalizedResource = GetCanonicalizedResource(request.RequestUri, storageAccountName); messageSignature = String.Format("{0}\n{1}\n{2}\n{3}\n{4}\n{5}\n{6}\n{6}\n{7}\n{8}\n{9}\n{10}\n{11}\n{12}{13}", method, contentEncoding, contentLanguage, contentLength, contentMd5, contentType, "", // included in the header, so empty here ifModifiedSince, ifMatch, ifNoneMatch, ifUnmodifiedSince, range, canonicalizedHeaders, canonicalizedResource ); } var signatureBytes = Encoding.UTF8.GetBytes(messageSignature); var hmac = new HMACSHA256(Convert.FromBase64String(_storageKey)); var authorizationHeader = "SharedKey " + storageAccountName + ":" + Convert.ToBase64String(hmac.ComputeHash(signatureBytes)); return authorizationHeader; }
SAS格式
这种SAS token可限制的更多,不止是过期时间,还包括了可允许访问的IP地址以及更具体的权限。Azure管理门户上提供了直接生成此类token的功能,如下图所示:
因为这种token对于不同访问级别(服务级别和账户级别),以及不同的存储服务(Blob,File,Queue和Table)有不同的格式,我后面将单独写一篇文章来介绍。
参考文章:
- https://docs.microsoft.com/en-us/rest/api/storageservices/Authentication-for-the-Azure-Storage-Services
- https://docs.microsoft.com/en-us/azure/storage/storage-dotnet-shared-access-signature-part-1
- https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-sas
- https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-security