如何生成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} + " " + 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 + "
" + 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;
}
View Code

而Storage则要复杂得多,而且有两种格式。一种是用做Authorization头部的SharedKey格式,另一种则是可放置在URL中的SAS格式

SharedKey格式

格式为

SharedKey {AccountName}:{Signature}

而且Table和其他几种存储服务的签名字符串格式还不一样。

Blob,Queue和File

StringToSign = VERB + " " +
         Content-Encoding + " " +
           Content-Language + " " +
        Content-Length + " " +
        Content-MD5 + " " +
        Content-Type + " " +
                        Date + " " +
                        If-Modified-Since + " " +
                        If-Match + " " +
                        If-None-Match + " " +
                        If-Unmodified-Since + " " +
                        Range + " " +
                        CanonicalizedHeaders +
                        CanonicalizedResource;

Table

StringToSign = VERB + " " +
                        Content-MD5 + " " +
                        Content-Type + " " +
                        Date + " " +
                        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("
", String.Empty);
            builder.Append(separator);
            builder.Append(trimmedValue);
            separator = ",";
        }
        sb.Append(builder.ToString());
        sb.Append("
");
    }
    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;
}
View Code
构建标准化的资源字符串
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("
");
        canonicalizedResourceStrBuilder.Append(builder3.ToString());
    }
    return canonicalizedResourceStrBuilder.ToString();
}
View Code
获取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}
{1}
{2}
{3}
{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}
{1}
{2}
{3}
{4}
{5}
{6}
{6}
{7}
{8}
{9}
{10}
{11}
{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;
}
View Code

SAS格式

这种SAS token可限制的更多,不止是过期时间,还包括了可允许访问的IP地址以及更具体的权限。Azure管理门户上提供了直接生成此类token的功能,如下图所示:

因为这种token对于不同访问级别(服务级别和账户级别),以及不同的存储服务(Blob,File,Queue和Table)有不同的格式,我后面将单独写一篇文章来介绍。

参考文章:

原文地址:https://www.cnblogs.com/hula100/p/7202705.html