CMDB API验证
为什么做API验证
API验证是防止数据在传输的过程中,保证数据不被篡改
如何设计的API验证
灵感来源于Torando中加密Cookie的源码,主要是生成加密的随机字符串。
MD5(key+time)|time
就是把秘钥和客户端的当前时间通过MD5进行加密,同时把当前时间发送到API
然后自己在服务端设计了三关验证的设计:
- 时间,
- 算法规则,验证密文
- 已访问的记录
想API获取数据的时候,需要进行验证是否是合法的请求,为了达到这一目的,采用的基础方案是在装机的时候在服务器中和客户端中都保存一份秘钥,客户端想要获取数据的时候需要携带秘钥过来,GET请求的时候通过请求头发送。
API验证之字符串+请求头
客户端向API发送GET请求的时候可以在请求有中加数据,是字典的格式headers={},在Django中,自己定义的请求头发送过去后,会转换成大写,并在前面加上HTTP_。
客户端,header中的OpenKey中间不能有下划线,否则后台接收不到
import requests
key = 'sdflajskldf9asfdalsdf9sdgsdgs55523asdf'
response = requests.get("http://127.0.0.1:8000/api/asset.html", headers={'OpenKey': key})
print(response.text)
服务端接收数据中获取请求头的中的内容,通过request.MEAT
可以获取全部的请求信息,是一个字典类型。
def asset(request):
for k, v in request.META.items():
print(k, v)
if request.method == "GET":
aaa = "重要的数据"
return HttpResponse(aaa)
HTTP_OPENKEY sdflajskldf9asfdalsdf9sdgsdgs55523asdf
当客户端发送数据的请求头中没有和服务端的key匹配的时候,不通过验证
key = request.META.get('HTTP_OPENKEY')
if settings.AUTH_KEY != key:
return HttpResponse('不能通过验证')
if request.method == "GET":
aaa = "重要的数据"
return HttpResponse(aaa)
动态令牌
上面的是静态的,当网络请求被截获后,别人就可以可以利用这个漏洞。需要改良成动态的令牌,每次访问的时候客户端发送不同的随机字符串并进行加密。要动态用当前时间,或者是UUID
- 利用当前时间和key形成动态的随机字符串
- 通过MD5进行加密
- 把当前时间发送到API,API利用这个时间进行MD5加密,然后比较密文
客户端
import requests
import time
import hashlib
ctime = time.time()
key = 'sdflajskldf9asfdalsdf9sdgsdgs55523asdf'
new_key= '%s|%s'%(key,ctime)
m = hashlib.md5()
m.update(bytes(new_key,encoding='utf8')) # python3中是字节
md5_key = m.hexdigest() # 加密后是字符串
md5_key_time = '%s|%s'%(md5_key,ctime) # 把当前时间发送后API,API利用时间进行加密,最后比较的是密文
response = requests.get("http://127.0.0.1:8000/api/asset.html", headers={'OpenKey': md5_key_time})
print(response.text)
服务端
- 服务端接收到客户端发送的数据中包含MD5和客户端用于加密的时间
- 服务端利用客户端发送过来的时间进行MD5加密
- 客户端和服务端进行密文的验证
def asset(request):
client_md5_citme = request.META.get('HTTP_OPENKEY') # 客户端发送过来的MD5和用于加密的时间
# print(client_md5_citme)
client_md5_key,client_time = client_md5_citme.split('|') # 进行分割
temp = '%s|%s'%(settings.AUTH_KEY,client_time)
m = hashlib.md5()
m.update(bytes(temp, encoding='utf8'))
server_md5_key = m.hexdigest() # 服务端利用客户端发送的时间进行MD5加密的数据
if server_md5_key!= client_md5_key:
return HttpResponse('不能通过验证')
if request.method == "GET":
aaa = "重要的数据"
return HttpResponse(aaa)
API三关验证
上面存在的问题,当被加密的md5_key_time别截获任意一个后(有成千上万个),别人在任何时候都能发送请求,服务端都能返回真实数据,这是一个很大的问题。
解决方案:
- 时间,服务端时间和客户端发送过来的时间进行比较,超过10s后,认为失效
- 在服务端维护一个已访问列表,仅仅是10s内的,超过后删除,如果有同样的再次访问的话,认为是被被人获取的,不通过验证
第一关:时间
- 在服务端进行判断,如果发送过来的客户端的时间和当前时间比,超过10s后,排除
服务端
def asset(request):
client_md5_citme = request.META.get('HTTP_OPENKEY') # 客户端发送过来的MD5和用于加密的时间
# print(client_md5_citme)
client_md5_key,client_time = client_md5_citme.split('|') # 进行分割
temp = '%s|%s'%(settings.AUTH_KEY,client_time)
m = hashlib.md5()
m.update(bytes(temp, encoding='utf8'))
server_md5_key = m.hexdigest() # 服务端利用客户端发送的时间进行MD5加密的数据
client_time = float(client_time) # 把字符串类型的转化成数字
server_time = time.time()
if server_time - client_time > 10:
return HttpResponse("小伙子,超时了")
客户端
客户端能正常发送,如果在10s内,客户端发送的秘钥被获取,并在10s内被请求,也是可以通过的
下面的key是黑客获取的秘钥,并在10s中内向API发送请求,同样是可以获取到数据的
import requests
key = '582d3d4bd95997954f70ae6906732de3|1501941968.090018'
response = requests.get("http://127.0.0.1:8000/api/asset.html", headers={'OpenKey': key})
print(response.text)
第二关:验证规则
这里就是在服务端进行MD5的验证
第三关: 建立已经访问的列表
如果是只采用时间的话,是有缺陷的,所以通过建立一个已经访问的列表进行进一步限制。即便是黑客获取了秘钥,当黑客发送请求的时候就能阻止黑客的请求
- 在全局建立已经访问过的列表,这里采用字典的形式,key是客户端发送过来的密钥,value是超时时间
- 后期使用redis memcache可以直接设置超时时间后自动回删除,这里是存在内存中,自己通过判断进行删除
# 在全局(存在内存中)建立已访问列表,格式是{'key秘钥|时间':超时时间} 后期可以用redis memcache等直接设置超时时间
api_key_record = {
}
# 在进入第三关之前删除api_key_record中的超时的值
# 字典不能通过for循环中删除
for k in list(api_key_record.keys()): # 需要把字典的keys()转换成列表
v = api_key_record[k]
if server_time > v:
del api_key_record[k] # 如果超时,则删除api_key_record中相应的记录
# 第三关:建立访问列表(全局)
if client_md5_citme in api_key_record:
return HttpResponse('第三关没有通过验证')
else:
api_key_record[client_md5_citme] = client_time + 10 # 设置超时时间 10s后超时
关于字典删除的补充
字典、列表在for循环的过程中是不同删除的,在for循环的时候,就已经建立了需要遍历的数据,在循环中删除,就没法迭代了
例如:
下面的程序会报错:RuntimeError: dictionary changed size during iteration
# 字典 列表等在循环的时候不能删除
api_dict = {'k1':'v1','k2':'v2','k3':'v3'}
for k,v in api_dict.items():
if v == 'v2':
del api_dict[k] # RuntimeError: dictionary changed size during iteration
解决方法:
- deepcopy
- list(api_dict.keys())
- 看下面的输出结果可知字典的keys()得到的是字典一个一种列表,通过list转换成列表
- 循环获取字典中的value进行比较,然后进行删除
api_dict = {'k1': 'v1', 'k2': 'v2', 'k3': 'v3'}
print(api_dict.keys()) # dict_keys(['k1', 'k2', 'k3'])
print(list(api_dict.keys())) # 通过list转换成列表
for k in list(api_dict.keys()):
v = api_dict[k] # 获取字典中所有的值value
if v == 'v2':
del api_dict[k]
print(api_dict)
结果:
dict_keys(['k1', 'k2', 'k3'])
['k1', 'k2', 'k3']
{'k1': 'v1', 'k3': 'v3'}
API三关验证总结
流程:
客户端和服务端中都存放一份相同的随机字符串,客户端发请求的时候把随机字符串和当前时间进行MD5加密,同时带着当前时间通过请求头发送到API,进入三关验证。
- 第一关是时间验证 (验证服务器当前时间和客户端发送过来的时间,超过10s后,验证不通过)
- 第二关是MD5规则验证(服务端把自己的密钥同客户端发送过来的时间进行MD5加密,进行密文的比较)
- 第三关是访问列表验证(从访问列表中查询是否存在,如果存在,验证不通过,否则把当前值存到列表中,并设置超时时间),这里的时间可以设置成2S
# 在全局(存在内存中)建立已访问列表,格式是{'key秘钥|时间':超时时间} 后期可以用redis memcache等直接设置超时时间
api_key_record = {
}
def asset(request):
client_md5_citme = request.META.get('HTTP_OPENKEY') # 客户端发送过来的MD5和用于加密的时间
# print(client_md5_citme)
client_md5_key,client_time = client_md5_citme.split('|') # 进行分割
# 第一关:时间验证
client_time = float(client_time) # 把字符串类型的转化成数字
server_time = time.time()
if server_time - client_time > 10:
return HttpResponse("小伙子,超时了,第一关没有通过验证")
# 第二关:规则验证
temp = '%s|%s'%(settings.AUTH_KEY,client_time)
m = hashlib.md5()
m.update(bytes(temp, encoding='utf8'))
server_md5_key = m.hexdigest() # 服务端利用客户端发送的时间进行MD5加密的数据
if server_md5_key!= client_md5_key:
return HttpResponse('第二关没有通过验证')
# 在进入第三关之前删除api_key_record中的超时的值
# 字典不能通过for循环中删除
for k in list(api_key_record.keys()): # 需要把字典的keys()转换成列表
v = api_key_record[k]
if server_time > v:
del api_key_record[k] # 如果超时,则删除api_key_record中相应的记录
# 第三关:建立访问列表(全局)
if client_md5_citme in api_key_record:
return HttpResponse('第三关没有通过验证')
else:
api_key_record[client_md5_citme] = client_time + 10 # 设置超时时间 10s后超时,然后从列表中删除
if request.method == "GET":
aaa = "重要的数据"
return HttpResponse(aaa)