如何生成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(, , );
var week = * * * ;
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 > )
{
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 = ,
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
如何生成Azure SAS Token的更多相关文章
- 使用PowerShell创建Azure Storage的SAS Token访问Azure Blob文件
Azure的存储包含Storage Account.Container.Blob等具体的关系如下: 我们常用的blob存储,存放在Storage Account的Container里面. 目前有三种方 ...
- jwt认证生成后的token如何传回后端并解析的详解
jwt认证生成后的token后端解析 一.首先前端发送token token所在的位置headers {'authorization':token的值',Content-Type':applicati ...
- Azure China (10) 使用Azure China SAS Token
<Windows Azure Platform 系列文章目录> 本文介绍的是国内由世纪互联运维的Azure China 注意:本文介绍的是Azure China Storage Priva ...
- springcloud +spring security多种验证方式之第三方token生成自己的token通过校验和自己的表单验证大体流程
步骤: 1.继承 WebSecurityConfigurerAdapter.class,其中使用两个过滤器,一个spring scurity自带的UsernamePasswordAuthenticat ...
- jwt认证生成后的token后端解析
一.首先前端发送token token所在的位置headers {'authorization':token的值',Content-Type':application/json} 在ajax写 //只 ...
- token是个什么东西?怎样生成并携带token
什么是token及怎样生成token 转载自:https://www.cnblogs.com/lufeiludaima/p/pz20190203.html 什么是token Token是服务端生成的 ...
- ECDSA密钥对生成以及在Token中的应用
1 概述 本文主要讲述了如何利用Openssl生成ECDSA密钥对,并利用Auth0库进行Token生成及验证的过程. 2 ECDSA 2.1 简介 ECC(Elliptic Curve Crypto ...
- 生成一个唯一token
$token = md5(uniqid(rand(), true));
- 根据UUID和MD5, 生成可以用作Token的字符串
import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util. ...
随机推荐
- R中用黏贴板读取excel数据以及用R生成人名
本人尚在超级菜鸟级别,中间有些代码写的很low,请不要见笑! 1.今天先跟大家分享一个小技巧,用黏贴板将excel中的数据读进R 首先选中要读入R的数据 如图,上面是我分析的病人相关资料,Ctrol+ ...
- Redis大幅性能提升之Batch批量读写
Redis大幅性能提升之Batch批量读写 提示:本文针对的是StackExchange.Redis 一.问题呈现 前段时间在开发的时候,遇到了redis批量读的问题,由于在StackExchange ...
- 转自52 梦回凉亭的她 Java常见问题,面试题
收集整理分享# 相关概念## 面向对象的三个特征封装,继承,多态.这个应该是人人皆知.有时候也会加上抽象.## 多态的好处允许不同类对象对同一消息做出响应,即同一消息可以根据发送对象的不同而采用多种不 ...
- nodejs 路径
在学习的时候遇到了 一些路劲方面的疑惑 便查询了一些 module.filename:开发期间,该行代码所在的文件.__filename:始终等于 module.filename.__dirname: ...
- 关于安卓百度地图SDK报错:Multiple dex files define Lcom/baidu/android/bbalbs/common/a/a;
1.找到.jar包 2.右键,用WinRAR打开 3.打开com/baidu/ 4.保留location,其他全删掉 5.这样将不会报错,可以运行了!!!
- solr学习笔记section2-solr单机(节点)简单的core操作
在上一节中我们已经成功部署和运行了一个solr应用,那么我们就可以通过这个正在运行的solr来创建一些文档,并进行搜索. 首先介绍一下core这个概念,core在solr中类似与关系型数据库中一张表的 ...
- Linux根目录各个文件夹介绍及说明
/bin 二进制可执行命令 /dev 设备特殊文件 /etc 系统管理和配置文件 /etc/rc.d 启动的配置文件和脚本 /home 用户主目录的基点,比如用户user的主目录就是/home/use ...
- Python pycrypto 加密与解密
参考: python 使用 pycrypto 实现 AES 加密解密 参考: 分组对称加密模式:ECB/CBC/CFB/OFB 代码示例 : import hashlib from Crypto.C ...
- 关于HTML学习整理(一)
新人,自己整理,第一次发,以后慢慢整理,欢迎指点,那些链接怎么做的,希望有人告知一下,谢谢! HTML页面写法,标签成对出现,可嵌套使用 <html> <head> <t ...
- Vulkan Tutorial 21 Staging buffer
操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Introduction 顶点缓冲区现在已经可以正常工作,但相比于显卡内部读取数据, ...