在调用阿里云 API 的时候,最让人头疼的就是 API 的签名(Signature)机制,阿里云在通用文档中也有专项说明,但是仅仅有基于 Java 的实现代码示例。所以这里基于 Python 来分析下。
基本步骤
- 构造规范化的请求字符串 (Canonicalized Query String)
- 构造被签名字符串 StringToSign
- 计算 HMAC 值
- 计算签名值
- 添加签名
理论部分的详细内容参考阿里云官方帮助文档吧
具体 Python 实现
API 请求原理
简单来说调用阿里云 API 就是一个 http 请求(大多数为 GET, 这里也是基于 GET 请求),只是后面要跟一堆参数,例如一个查看快照的请求为:
http://ecs.aliyuncs.com/?SignatureVersion=1.0&Format=JSON&Timestamp=2017-08-07T05%3A50%3A57Z&RegionId=cn-hongkong&AccessKeyId=xxxxxxxxx&SignatureMethod=HMAC-SHA1&Version=2014-05-26&Signature=%2FeGgFfxxxxxtZ2w1FLt8%3D&Action=DescribeSnapshots&SignatureNonce=b5046ef2-7b2b-11e7-a3c5-00163e001831&ZoneId=cn-hongkong-b
请求中所需要的公共参数(就是调用 API 都需要用到的参数)为:
SignatureVersion # 签名算法版本,目前为 1.0
Format # 返回消息的格式化方式,JSON or XML 默认值为 XML
Timestamp # 请求的时间戳,UTC时间,例如: 2013-01-10T12:00:00Z
AccessKeyId # 账号密钥 ID
SignatureMethod # 签名方式,目前为 HMAC-SHA1
Version # 版本号,为日期形式,例如: 2014-05-26 每个产品不同
Signature # 最难搞定的签名
SignatureNonce # 唯一随机数,防止网络攻击。不同请求间使用不同的随机数。
除了 Signature
之外,其它的参数都比较容易获得,有些甚至是固定的值,具体可以参考阿里云文档
除了公共参数之外,还需要具体接口(Action)的请求参数,每个 Action
接口的参数可以参考对应产品的接口文档,例如 DescribeLoadBalancers
而 Signature
是基于公共参数和接口参数的,所以比较复杂。
Signature 具体代码实现
构造规范化的请求字符串 (Canonicalized Query String)
- 构造 dict
Python 中体现参数一一对应的就是 dict, 创建一个 dict, 把请求参数些进去,这里简化参数只有这些:
>>> D = {
'Format':'JSON',
'Version':'2014-05-26',
'SignatureMethod':'HMAC-SHA1'
}
- 排序
由于签名要求唯一性,包括顺序,所以需要按照参数名称排序
>>> sortedD = sorted(D.items(), key=lambda x: x[0])
>>> sortedD
[('Format', 'JSON'),
('SignatureMethod', 'HMAC-SHA1'),
('Version', '2014-05-26')]
# 先通过 D.items() 转化为 List, 然后利用 sorted 方式按照 key 排序
- URL 编码
由于在标准请求字符串中需要使用 UTF-8 字符集,对请求参数名称和值中有些不符合规范的字符要进行 url 编码,具体规则为:
字符 AZ、az、0~9 以及字符“-”、“_”、“.”、“~”不编码;
其它字符编码成 %XY 的格式,其中 XY 是字符对应 ASCII 码的 16 进制表示。比如英文的双引号(”)对应的编码为 %22;
对于扩展的 UTF-8 字符,编码成 %XY%ZA… 的格式;
英文空格( )要编码成 %20,而不是加号(+)。
注意:一般支持URL编码的库(比如 Java 中的 java.net.URLEncoder)都是按照 “application/x-www-form-urlencoded”的 MIME 类型的规则进行编码的。实现时可以直接使用这类方式进行编码,把编码后的字符串中加号(+)替换成 %20、星号(*)替换成 %2A、%7E 替换回波浪号(~),即可得到上述规则描述的编码字符串。
这里使用 python 中的 urllib
库来进行编码:
>>> def percentEncode(str):
res = urllib.quote(str.decode(sys.stdin.encoding).encode('utf8'), '')
res = res.replace('+', '%20')
res = res.replace('*', '%2A')
res = res.replace('%7E', '~')
return res
# 这里构造一个编码函数,对一个字符串进行编码,返回编码后的字符串
- 生成标准化请求字符串
>>> canstring = ''
>>> for k,v in sortedD:
canstring += '&' + percentEncode(k) + '=' + percentEncode(v)
>>> canstring
'&Format=JSON&SignatureMethod=HMAC-SHA1&Version=2014-05-26'
构造被签名字符串 StringToSign
规则为:
StringToSign=
HTTPMethod + “&” +
percentEncode(“/”) + ”&” +
percentEncode(CanonicalizedQueryString)
所以在这个实例中
>>> stringToSign = 'GET&%2F&' + percentEncode(canstring[1:])
>>> stringToSign
'GET&%2F&Format%3DJSON%26SignatureMethod%3DHMAC-SHA1%26Version%3D2014-05-26'
# >>> percentEncode(“/”)
# %2F
计算 HMAC 值
>>> access_key_secret = 'access_key_secret'
>>> h = hmac.new(access_key_secret + "&", stringToSign, sha1)
>>> h
# access_key_secret 是通过阿里云账号中的 AK 中获取的,和 access_key_id 对应,测试的时候使用的是 'access_key_secret'
计算签名值
>>> signature = base64.encodestring(h.digest()).strip()
>>> signature
'sq8LVH+ZItZiVQ0/rVnHV1kP/BE='
到此生成了 signature
签名
添加签名
>>> D['Signature'] = signature
>>> D
{'Format': 'JSON',
'Signature': 'sq8LVH+ZItZiVQ0/rVnHV1kP/BE=',
'SignatureMethod': 'HMAC-SHA1',
'Version': '2014-05-26'}
所以在这个实例中,最终请求的 url 为
>>> url = 'http://ecs.aliyuncs.com/?' + urllib.urlencode(D)
>>> url
'http://ecs.aliyuncs.com/?SignatureMethod=HMAC-SHA1&Version=2014-05-26&Signature=sq8LVH%2BZItZiVQ0%2FrVnHV1kP%2FBE%3D&Format=JSON'
拿到浏览器直接访问即可,得到结果为:
{"Message":"The input parameter \"Action\" that is mandatory for processing this request is not supplied.","RequestId":"129880D4-710D-4D2C-9F8B-12777FA1D3C6","HostId":"ecs.aliyuncs.com","Code":"MissingParameter"}
由于是测试环境,就给了三个参数,所以还少很多参数,正常来说把这些参数都加上,然后生成 signature
,组成 url 后直接访问就可以得到结果。