故事背景:项目要开放一下api给客户使用,接口都ready了,文档也写好了;对于open api,最基础的一项就是鉴权问题,如果鉴权ok了,对接工作可以稳步进行。同事把鉴权逻辑的文档写好了,但是客户发现用python写的接口总是报401,为了手把手帮客户解决问题,三年没再写python的我上场了。
签名与鉴权
签名逻辑:客户端把请求数据序列化后,转成bytes,然后用密钥加密,把签名放到http请求的header里。
鉴权逻辑:服务端从http请求的header中取出签名和请求数据,本地进行签名,对比两个签名结果是否一致。
代码
我的第一版鉴权代码如下:
#!/usr/bin/python3
# -*- coding: UTF-8 -*-
import hmac, base64, hashlib, requests, json
class Api_Auth(object):
def __init__(self):
# replace following params with your ak sk
self.ak = 'ak'
self.sk = bytes('sk', encoding='utf-8')
self.host = ""
def getSignKey(self, data):
sign = hmac.new(self.sk, data, hashlib.sha1).digest()
val = self.ak + ":" + base64.urlsafe_b64encode(sign).decode()
return val
def getRequestData(self, method, path, queryStr="", bodyStr="", contentType="application/json"):
data = method+" "+path
if queryStr != "":
data += "?"+queryStr
data += "\nHost: "+self.host
if bodyStr != "":
data += "\nContent-Type: " + contentType
data += "\n\n"
if bodyStr != "" and bodyStr != "application/octet-stream":
data += bodyStr
return data
# get status
class Api_Data(object):
def get_data(self):
url = ''
path = '/v1/status'
data = Api_Auth().getRequestData("GET", path, "", "")
signkey = Api_Auth().getSignKey(data.encode('utf-8'))
headers = {
'Authorization':signkey,
}
response = requests.get(url, headers=headers, data='')
return response.content.decode()
print(Api_Data().get_data())
测试了一个get请求,正常返回结果,完美!于是就把这个demo发布出去了。
然而!后面有用户反馈接口还是401,不过不是get接口,是post接口,我于是开始编写post接口并测试,代码如下
def post_data(self,jsonData):
url = ''
path = ''
data = Api_Auth().getRequestData("POST", path, "", str(jsonData))
signkey = Api_Auth().getSignKey(data.encode('utf-8'))
headers = {
'Authorization':signkey,
}
print(signkey)
测试发现确实不通;就觉得挺奇怪的,然后把签名前的data打印出来,用python签名代码和golang签名版本做对照(因为项目的server是用golang鉴权的),两者数据如下:
POST /v1/task
Host: aaa.com
Content-Type: application/json
{'name': 'test'}
POST /v1/task
Host: aaa.com
Content-Type: application/json
{"name":"test"}
当时我一看结果是一样的,python的字符串也支持单引号表示,然后就对比长度,又对比签名后base64之前和之后的bytes,发现确实不一致;又对比签名前data的bytes数据,发现只有少数几个地方不一致,ascii码分别为39和34,查ascii码表发现分别是单引号和双引号。这时候发现不对了,其实前面打印的结果就不一致,但是由于python的特性让我忽略了,序列后的结果应该完全一致才行,但为啥json的就是单引号呢。
结论
网上查完发现,对于一个dict,直接str转换是可以得到相应的字符串的,dict里的key或者字符串类型的value都是加单引号的,而对于golang的web接口,字符串是必须用双引号括起来的,所以解决方法是将dict序列化后的结果都带双引号,即使用json.dumps,并且在调用python的request.post方法时,传入的数据也必须是json.dumps转换后的结果。
更正后的代码为
def post_data(self,jsonData):
url = ''
path = ''
data = Api_Auth().getRequestData("POST", path, "", json.dumps(jsonData))
signkey = Api_Auth().getSignKey(data.encode('utf-8'))
headers = {
'Authorization':signkey,
}
print(signkey)
最后对比下str和json.dumps的区别