本文举例来说明API签名,并有具体实现流程,规则弄会,一通百通。
本文先用一个故事举例,方便理解,然后对整个流程做了逐步分析和局部代码实现,最后把代码整合起来,想直接看整合后代码的可以直接去最底。
一、故事引入签名认证原理(不要纠结为什么吃饭这么麻烦- -!)
有家饭店,只对会员开放,小明在饭店注册会员。
饭店给小明一个会员号"xiaoming"(签名中的ApiKey)并且永久有效。
又给小明一个令牌(签名中的SecretKey),令牌内容是"abcd",并告诉小明令牌只能自己知道,不可以告诉别人,又告知令牌30天后失效,失效后可以再来申请一个新的令牌(依然30天有效期)。
(饭店是知道小明的会员号和令牌的)
这天,小明要在饭店点餐,主食:米饭,炒菜:土豆丝, 于是给饭店发送了点菜短信,短信内容可以有多种方案,具体内容如下(这里只写两个短信方案):
方案一:
会员号:xiaoming
主食:米饭
炒菜:土豆丝
口令(signature):abcd(实际开发中,令牌abcd不会用于网络通讯中,
而是把会员号,主食,炒菜三个键值对加密拼接,
并用‘abcd’进行加密,
具体实现下面会说明,这里先了解流程)
方案二:
会员号:小明
主食:米饭
炒菜:土豆丝
时间(timestamp):12点(假设偏差10分钟,如果饭店收到消息比11点50早
或比12点10分晚,就认为是骗子)
随机码(nonce):7878(饭店每次都记录随机码,收到信息后看看随机码之前用过没,
如果用过,就认为是骗子)
口令(signature):abcd(实际开发中,令牌abcd不会用于网络通讯中,
而是会把上边5个键值对先加密,
再拼接,再用‘abcd’加密作为口令,
具体实现下面会说明,这里先了解流程)
饭店收到信息后核对无误,开始做饭。
有人会有疑问,如果骗子随便写个随机码,刚好之前没用过,岂不是就蒙混过关了??注意一点,口令的生成和随机码有关系,所以只要骗子拿不到令牌(secretkey),就无法生成正确的口令。
二、准备(用户注册申请密钥)
服务器会返回两个key
AccessKey: 身份标识,唯一,类似ID,用户名什么的
SecretKey:密钥,只保存在客户端和服务器,不做网络传递,一般都有有效期
本文中
accesskey="ch_improve"
secretkey="abcd"
三、客户端生成签名
1.准备参数
需要传递的参数有:
{
"accesskey": "ch_improve",
"name": "gaofushuai",
"city": "zhengzhou",
}
实际运用中,可以添加参数,例如
SignatureMethod="HmacSHA256"
timestamp=1572662767
method="POST"
nonce=7878
我以如下参数为例
{
"accesskey": "ch_improve",
"name": "gaofushuai",
"city": "zhengzhou",
"SignatureMethod": "HmacSHA256",
"timestamp": 1572662767,
"method": "POST",
"nonce": 7878
}
2.对参数进行排序并转为URL键值对格式字符串
首先需要对传递的参数按照键的字典升序进行排序,并得到URL键值对格式的字符串
SignatureMethod=HmacSHA256&accesskey=ch_improve&city=zhengzhou&method=POST&name=gaofushuai&nonce=7878×tamp=1572662767
SignatureMethod=HmacSHA256&accesskey=ch_improve&city=zhengzhou&method=POST&name=gaofushuai&nonce=7878×tamp=1572662767
python3代码实现
(1)字典升序排序
args = {
"accesskey": "ch_improve",
"name": "gaofushuai",
"city": "zhengzhou",
"SignatureMethod": "HmacSHA256",
"timestamp": 1572662767,
"method": "POST",
"nonce": 7878
}
args_order = sorted(args.items(), key=lambda x: x[0])
args_order的值如下
[('SignatureMethod', 'HmacSHA256'), ('accesskey', 'ch_improve'), ('city', 'zhengzhou'), ('method', 'POST'), ('name', 'gaofushuai'), ('nonce', 7878), ('timestamp', 1572662767)]
(2)转为URL字符串
import urllib.parse
args_str = urllib.parse.urlencode(args_order)
args_str的值如下,是一个长字符串
SignatureMethod=HmacSHA256&accesskey=ch_improve&city=zhengzhou&method=POST&name=gaofushuai&nonce=7878×tamp=1572662767
3.使用密钥SecretKey对URL格式的字符串args_str进行加密获取签名
常用加密方法有MD5、SHA256等等,本文采用HmacSha256加密
import hmac
secretkey="abcd"
# 先把secretkey和args_str转为byte类型
b_secretkey = secretkey.encode('utf-8')
b_args_str = args_str.encode('utf-8')
# 用密钥对URL格式的参数进行hmacsha256加密
signature = hmac.new(b_secretkey, b_args_str, digestmod='sha256').hexdigest()
signature签名的值如下
3623d8070662392772d10952ea39c277518546006e6649f4ea358671e51e06f7
有些服务器要求把signature签名字符串全部转为大写,转不转大写看接口要求,本文就不再转大写了
4.在原有参数基础上添加signature签名参数,得到最新的参数
{
"accesskey": "ch_improve",
"name": "gaofushuai",
"city": "zhengzhou",
"SignatureMethod": "HmacSHA256",
"timestamp": 1572662767,
"method": "POST",
"nonce": 7878,
"signature": "3623d8070662392772d10952ea39c277518546006e6649f4ea358671e51e06f7"
}
5.得到最终参数后,可以发送请求了,至于如何携带参数,基本有三种选择(URL字符串、请求头、请求体)
(1)URL键值对字符串
accesskey=ch_improve&name=gaofushuai&city=zhengzhou&
SignatureMethod=HmacSHA256×tamp=1572662767&meth
od=POST&nonce=7878&signature=3623d8070662392772d10952
ea39c277518546006e6649f4ea358671e51e06f7
比如访问http://127.0.0.1:8888,那么url就是
http://127.0.0.1:8888/?accesskey=ch_improve&name=gaofushuai
&city=zhengzhou&SignatureMethod=HmacSHA256×tamp=1572662767
&method=POST&nonce=7878&signature=3623d8070662392772d10952e
a39c277518546006e6649f4ea358671e51e06f7
(2)请求头中携带参数
可以自定义一个字段,比如"MyArgs",值就使用字典参数的json格式字符串,如下
Myargs: {"accesskey": "ch_improve", "name": "gaofushuai", "city": "zhengzhou", "SignatureMethod": "HmacSHA256", "timestamp": 1572662767, "method": "POST", "nonce": 7878, "signature": "3623d8070662392772d10952ea39c277518546006e6649f4ea358671e51e06f7"}
(3)请求体中携带参数
这个直接放入请求体就行,作为一般参数
四、服务器收到请求后验证
1.接受参数
{
"accesskey": "ch_improve",
"name": "gaofushuai",
"city": "zhengzhou",
"SignatureMethod": "HmacSHA256",
"timestamp": 1572662767,
"method": "POST",
"nonce": 7878,
"signature": "3623d8070662392772d10952ea39c277518546006e6649f4ea358671e51e06f7"
}
2.检查时间戳timestamp(如果不使用时间戳作为参数,则跳过此步)
假如约定时间戳误差范围为10分钟,也就是600秒,根据接收到的时间戳1572662767计算出时间戳范围
1572662767-600 ~~ 1572662767+600
获取服务器本地时间戳
import time
timestamp = time.time()
如果得到结果不在范围内,则拒绝访问
3.检查随机动态码nonce是否有效(如果不使用nonce作为参数,则跳过此步)
(1)nonce为客户端随机生成的验证码,当服务器接收到请求后,会把nonce存储到数据库中,一般使用redis,并设置一个有效期,一般和时间戳timestamp的失效时间保持一致,设为10分钟有效期。
(2)当服务器接收到请求后,用请求中nonce(本文为7878)和redis中的nonce集合做比较,如果已经存在,则拒绝访问接口,只有当10分钟之内第一次使用7878这个动态码,才判定访问有效。
4.验证签名signature
对除了signature之外的所有参数进行升序URL格式转换拼接,得到字符串再用密钥secretkey(本文是‘abcd’)加密,得到signature2,和请求中的signature做比较判断请求是否合法。
不一致则拒绝访问。
五、总结下生成签名认证请求参数生成的代码
import json # 导入json
import urllib.parse # 导入URL编译库
import hmac # 导入HMAC加密库
accesskey="ch_improve"
# 密钥,仅保存在客户端和服务端,不做网络传递,一般都有有效期,15天之类的
secretkey="abcd"
# 准备参数
# 时间戳timestamp可以用 timestamp = int(time.time()) 生成
# nonce是随机生成的验证码,例如"7878", "k9i8e7"
args = {
"accesskey": "ch_improve",
"name": "gaofushuai",
"city": "zhengzhou",
"SignatureMethod": "HmacSHA256",
"timestamp": 1572662767,
"method": "POST",
"nonce": 7878
}
# 对参数进行排序,按照键的字典升序排序
args_order = sorted(args.items(), key=lambda x: x[0])
# 得到args_order=[('SignatureMethod', 'HmacSHA256'), ('accesskey', 'ch_improve'), ('city', 'zhengzhou'), ('method', 'POST'), ('name', 'gaofushuai'), ('nonce', 7878), ('timestamp', 1572662767)]
# 对排好序的元祖列表进行URL转换
args_str = urllib.parse.urlencode(args_order)
# 得到字符串SignatureMethod=HmacSHA256&accesskey=ch_improve&city=zhengzhou&method=POST&name=gaofushuai&nonce=7878×tamp=1572662767
# 对密钥和字符串进行byte转换
b_secretkey = secretkey.encode('utf-8')
b_args_str = args_str.encode('utf-8')
# 用密钥abcd进行加密
signature = hmac.new(b_secretkey, b_args_str, digestmod='sha256').hexdigest()
# 得到签名3623d8070662392772d10952ea39c277518546006e6649f4ea358671e51e06f7
# 把签名添加到请求参数中作为新的请求参数
{
"accesskey": "ch_improve",
"name": "gaofushuai",
"city": "zhengzhou",
"SignatureMethod": "HmacSHA256",
"timestamp": 1572662767,
"method": "POST",
"nonce": 7878,
"signature": "3623d8070662392772d10952ea39c277518546006e6649f4ea358671e51e06f7"
}