接口的安全机制
本篇内容:
- 用户认证
- 数字签名
一、用户认证
在测试Web接口时,不管使用的是接口工具还是Requests库都提供了Auth的选项/参数,这个选项提供了username和password的选项,但这里Auth的用户名和密码与系统登录的用户名和密码有区别。登录的用户名和密码是作为接口的参数来传输,而Auth不是,它仍然包含在request请求中。
1.1、开发带Auth接口
重新创建../sign/views_if_sec.py视图文件
接口的处理逻辑主要由views层完成。所以这里只提供views层的实现
views_if_sec.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:tian
from django.contrib import auth as django_auth
from django.http import JsonResponse
from sign.models import Event,Guest
import hashlib,base64,time
#用户认证
def user_auth(request):
# request.META是一个Python字典,包含了所有本次HTTP请求的Header信息,比如用户认证、IP地址和用户Agent(通常是浏览器的名称和版本号)
get_http_auth = request.META.get('HTTP_AUTHORIZATION',b'')
# HTTP_AUTHORIZATION用于获取HTTP authorization
print("get_http_auth是什么",get_http_auth)
auth = get_http_auth.split() #分割成list
print(auth)
try:
auth_parts = base64.b64decode(auth[1]).decode('utf-8').partition(':') #解码
print(auth_parts)
except IndexError:
return "null"
userid, password = auth_parts[0],auth_parts[2]
user = django_auth.authenticate(username=userid,password=password)
if user is not None and user.is_active:
django_auth.login(request,user)
return "success"
else:
return "fail"
#发布会查询接口--增加用户认证
def get_event_list(request):
auth_result = user_auth(request)
if auth_result == "null":
return JsonResponse({'status':10011,'message':'user auth null'})
elif auth_result == "fail":
return JsonResponse({'status':10012,'message':'user auth fail'})
eid = request.GET.get("eid","") #发布会id
name = request.GET.get('name',"") #发布会名称
if eid == '' and name =='':
return JsonResponse({'status':10021,'message':'prarameter_error'})
if eid !='':
event = {}
try:
result = Event.objects.get(id=eid) #get()方法是从数据库获取一个匹配结果,返回一个对象,如果不存在会报错
except Exception as e:
return JsonResponse({'status':10022,'message':'quer result is empty'})
else:
event['eid'] = result.id
event['name'] = result.name
event['limit'] = result.limit
event['status'] = result.status
event['address'] = result.address
event['stat_time'] = result.start_time
return JsonResponse({'status':200,'message':'success','data':event})
if name !='':
datas =[]
results = Event.objects.filter(name__contains=name)#filter()方法从数据库取得匹配结果,返回一个对象列表,如果不存在返回[]
if results:
for r in results:
event = {}
event['eid'] = r.id
event['name'] = r.name
event['limit'] = r.limit
event['status'] = r.status
event['address'] = r.address
event['stat_time'] = r.start_time
datas.append(event)
return JsonResponse({'status':200,'message':'success','data':datas})
else:
return JsonResponse({'status':10022,'message':'query result is empty'})
代码解释:
get_http_auth = request.META.get('HTTP_AUTHORIZATION',b'')
request.META是一个Python字典,包含了所有本次HTTP请求的Header信息,比如用户认证、IP地址和Agent(浏览器的名称和版本号)等;HTTP_AUTHORIZATION用于获取HTTP authorization(授权)
get_http_auth值为:Basic dGlhbjoxMjM0NTZ0aWFu ; auth = get_http_auth.split()已空格进行分割,分割后为列表['Basic', 'dGlhbjoxMjM0NTZ0aWFu']
auth_parts = base64.b64decode(auth[1]).decode('utf-8').partition(':') #解码且:划分 partition(':')已什么划分参数可以自定义
解码后:('tian', ':', '123456tian')
userid, password = auth_parts[0],auth_parts[2] 取出元组中对应的用户名和密码;
user = django_auth.authenticate(username=userid,password=password) #执行django自带的验证模块
result = Event.objects.get(id=eid) #get()方法从数据库获取一个匹配结果,不存在会报错 ;
results = Event.objects.filter(name__contains=name) #filter()方法从数据库取的匹配结果,返回一个对象列表,不存在返回[]空列表
在.../sign/urls.py中添加新的安全接口指向
url.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:tian
from django.conf.urls import url
from sign import views_if
from sign import view_if_sec
urlpatterns = [
url(r'^add_event/',views_if.add_event,name='add_event'),
url(r'^add_guest/',views_if.add_guest,name="add_guest"),
url(r'^get_event_list',views_if.get_event_list,name='get_event_list'),
url(r'^get_guest_list',views_if.get_guest_list,name='get_guest_list'),
url(r'^user_sign/',views_if.user_sign,name='user_sign'),
url(r'^sec_get_event_list/',view_if_sec.get_event_list,name='get_event_list')
]
PS:需要说明,这种认证方式是一种相对比较弱的认证方式,安全性较低。
1.2、编写接口文档

1.3、编写接口用例

get_event_auth_list_test.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:tian
import unittest,requests
class GetEventListTest(unittest.TestCase):
''' 查询发布会信息(带用户认证)'''
def setUp(self):
self.base_url = "http://127.0.0.1:8001/api/sec_get_event_list/"
self.user_auth = ('tian','123456tian')
def test_get_event_list_auth_null(self):
''' auth为空'''
r =requests.get(self.base_url,params={'eid':''})
result = r.json()
self.assertEqual(result['status'],10011)
self.assertEqual(result['message'],'user auth null')
def test_get_event_list_auth_error(self):
'''auth错误'''
r = requests.get(self.base_url,auth=('abc','123'),params={'eid':''})
result =r.json()
self.assertEqual(result['status'],10012)
self.assertEqual(result['message'],"user auth fail")
def test_get_event_list_eid_null(self):
'''eid为空'''
r = requests.get(self.base_url,auth=self.user_auth,params={'eid':''})
result = r.json()
self.assertEqual(result['status'],10021)
self.assertEqual(result['message'],"prarameter_error")
def test_get_event_list_eid_error(self):
'''eid错误'''
r = requests.get(self.base_url,auth=self.user_auth,params={'eid':998})
result = r.json()
self.assertEqual(result['status'],10022)
self.assertEqual(result['message'],'quer result is empty')
def test_get_event_list_eid_success(self):
'''根据eid查询成功'''
r =requests.get(self.base_url,auth=self.user_auth,params={'eid':1})
result =r.json()
self.assertEqual(result['status'],200)
self.assertEqual(result['message'],'success')
self.assertEqual(result['data']['name'],"红米发布会")
self.assertEqual(result['data']['address'],"北京会展中心")
def test_get_event_list_name_result_null(self):
'''关键字abc查询'''
r = requests.get(self.base_url,auth=self.user_auth,params={'name':'abc'})
result = r.json()
self.assertEqual(result['status'],10022)
self.assertEqual(result['message'],'query result is empty')
def test_get_event_list_name_find(self):
'''关键字发布会模糊查询'''
r = requests.get(self.base_url,auth=self.user_auth,params={'name':'发布会'})
result = r.json()
self.assertEqual(result['status'],200)
self.assertEqual(result['message'],'success')
self.assertEqual(result['data'][0]['name'],'红米发布会')
self.assertEqual(result['data'][0]['address'],'北京会展中心')
二、数字签名
在使用HTTP/SOAP协议传输数据的时候,签名作为其中一个参数,可以起到关键作用:
2.1、鉴权通过客户的密钥,服务端的密钥匹配:
例如:一个接口传参为:http://127.0.0.1:8001/api/?a=1&b=2 ,假设签名的密钥为:@admin123,加上签名之后的接口参数为:http://127.0.0.1:8001/sign/?a=1&b=2&sign=@admin123
显然,sign参数明文传输是不安全的,使用一般通过加密算法进行加密:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:tian
import hashlib
sign_str = "@admin123"
md5 = hashlib.md5(bytes(sign_str,encoding="utf-8"))
md5.update("adman**&&".encode("utf-8"))
sign_str = md5.hexdigest()
print(sign_str)
执行:4a377d8ee2c6153d3f6df9b250d3bde8
所以单做鉴权带签名的接口为:http://127.0.0.1:8001/api/?a=1&b=2&sign=4a377d8ee2c6153d3f6df9b250d3bde8 ;
因为MD5算法不可逆向,所以当服务器收到请求后,同样需要对@admin123进行MD5加密,然后对比与调用者传来的sign加密串是否一致,从而来鉴别调用者是否有权限调用该接口 ;
2.2、数据防篡改
参数是明文传输,将测试及密钥作为签名与服务器匹配
例如:
http://127.0.0.1:8001/sign/?a=1&b=2 加密方式比以前更复杂,假设签名密匙为:@admin123
签名的明文为:a=1&b=2&api_key@admin123 ,再次通过MD5值进行加密:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:tian
import hashlib
sign_str = "a=1&b=2&api_key=@admin123"
md5 = hashlib.md5(bytes(sign_str,encoding="utf-8"))
md5.update("adman**&&".encode("utf-8"))
sign_str = md5.hexdigest()
print(sign_str)
所以带参数的接口串为:
http://127.0.0.1:8001/sign/?a=1&b=2&sign=af7a9e6e84b0a89d3e70e0cf335b09d0
因为整个接口的参数做了加密,所有只要任意一个参数改变,那签名的验证就会失败,从而起到了鉴权及数据完整性的保护;不过接口全参数签名也存在弊端,服务器端必须已知客户端的接口参数和值,否则签名验证就会失败,但一般接口在设计时对客户端请求的参数并不完全已知。
2.3、开发接口
打开../sign/views_if_security.py视图文件
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:tian
import hashlib,json
from sign.models import Event,Guest
import time
from django.http import JsonResponse
#用户签名+时间戳
def user_sign(request):
if request.method == "POST":
client_time = request.POST.get('time','') #客户端时间戳
client_sign = request.POST.get('sign','') #客户端签名
else:
return "error"
if client_time =='' or client_sign == '':
return 'sign null'
#服务器时间
now_time = time.time() #
server_time = str(now_time).split('.')[0]
#获取时间差
time_difference = int(server_time) - int(client_time)
if time_difference >= 60:
return "timeout"
#签名检查
api_key = "&Guest-Bugmastar"
sign_str ="{0}{1}".format(client_time,api_key)
md5 = hashlib.md5(bytes(sign_str,encoding="utf-8"))
md5.update('tian%%**'.encode('utf-8'))
sever_sign = md5.hexdigest()
if sever_sign != client_sign:
return 'sign_fail'
else:
return "sign_right"
#添加发布会接口 -- 增加签名+时间戳
def add_event(request):
sign_result = user_sign(request)
if sign_result == "error":
return JsonResponse({'status':10011,'message':'request error'})
elif sign_result == 'sign null':
return JsonResponse({'status':10012,'message':'user sign null'})
elif sign_result =='timeout':
return JsonResponse({'status':10013,'message':'user sign timeout'})
elif sign_result =='sign_fail':
return JsonResponse({'status':10014,'message':'user sign error'})
eid = request.POST.get('eid','') #发布会id
name = request.POST.get('name','') #发布会标题
limit = request.POST.get('limit','') #限制人数
status = request.POST.get('status','') #状态
address = request.POST.get('address','') #地址
start_time = request.POST.get('start_time','') #发布会时间
if eid =='' or name =='' or limit =='' or address =='' or start_time == '':
return JsonResponse({'status':10021,'message':'parameter error'})
result = Event.objects.filter(id=eid)
if result:
return JsonResponse({'status':10022,'message':'event id already exists'})
result = Event.objects.filter(name=name)
if result:
return JsonResponse({'status':10023,'message':'event name already exists'})
if status =='':
status = 1
try:
Event.objects.create(id=eid,name=name,limit=limit,address=address,status=int(status),start_time=start_time)
except Exception as e:
error ='start_time format error It must be in YYYY-MM-DD HH:MM:SS format'
return JsonResponse({'status':10024,'message':error})
return JsonResponse({'status':200,'message':'add event success'})
代码解释:
通过POST方法获取两个参数time和sign两个参数,并判断它们其中任一一个为空,则返回sign null;
接下需要获取服务器端时间戳,因时间戳精度太高我们只需小数点前面的位数,所以使用split('.')函数进行分割,然后通过下标[0]获取;
获取时间差,服务器端时间(转换int类型) 减去 客户端时间 (转换int类型),时间差超过60秒,则返回timeout。
服务器端生成签名,(密匙和客户端时间戳)两者拼接成一个新字符串,并且通过MD5进行加密。
服务端端生成加密字符串和客户端加密字符串比较是否相等,相等说明签名验证通过,不等说明失败返回sign_fail。
在../sign/urls.py文件中添加新的安全接口路由
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:tian
from django.conf.urls import url
from sign import views_if
from sign import view_if_sec
from sign import views_if_security
urlpatterns = [
#guest system interface
#ex :/api/add_event/
url(r'^add_event/',views_if.add_event,name='add_event'),
url(r'^add_guest/',views_if.add_guest,name="add_guest"),
url(r'^get_event_list',views_if.get_event_list,name='get_event_list'),
url(r'^get_guest_list',views_if.get_guest_list,name='get_guest_list'),
url(r'^user_sign/',views_if.user_sign,name='user_sign'),
url(r'^sec_get_event_list/',view_if_sec.get_event_list,name='get_event_list'),
url(r'^sec_add_event/',views_if_security.add_event,name='add_event')
]
2.4、编写接口文档
添加发布会接口文档

2.5、编写接口用例

post_event_test.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:tian
import unittest,time,requests
import hashlib
import os,sys
path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0,path)
class AddEventTest(unittest.TestCase):
'''添加发布会接口(数字验证)'''
def setUp(self):
self.base_url = "http://127.0.0.1:8001/api/sec_add_event/"
# app_key
self.api_key ="&Guest-Bugmastar" # key
now_time = time.time() #当前时间戳
self.client_time = str(now_time).split(".")[0] #分割保留小数点前面的数字且转换为str类型
now_time = self.client_time
key_cline_time ="{0}{1}".format(self.client_time,self.api_key)#时间戳和key字符串拼接key_cline_time
md5 = hashlib.md5(bytes(key_cline_time,encoding="utf-8")) #md5加密
md5.update('tian%%**'.encode('utf-8')) #md5加密后再加盐加料
self.result_md5 = md5.hexdigest() #最终的数字签名self.result_md5
def tearDown(self):
print(self.result)
def test_add_event_sign_null(self):
'''签名参数为空'''
payload = {'eid':1,'':'','limit':'','address':'','start_time':'','time':'','sign':''}
r = requests.post(self.base_url,data=payload)
self.result = r.json()
self.assertEqual(self.result['status'],10012)
self.assertEqual(self.result['message'],'user sign null')
def test_add_event_time_out(self):
'''请求超时'''
now_time = str(int(self.client_time)-61)
payload = {'eid':1,'':'','limit':'','address':'','start_time':'','time':now_time,'sign':'abc'}
r = requests.post(self.base_url,data=payload)
self.result =r.json()
self.assertEqual(self.result['status'],10013)
self.assertEqual(self.result['message'],'user sign timeout')
def test_add_event_sign_error(self):
'''签名错误'''
payload = {'eid':1,'':'','limit':'','address':'','start_time':'','time':self.client_time,'sign':'abc'}
r = requests.post(self.base_url,data=payload)
self.result = r.json()
self.assertEqual(self.result['status'],10014)
self.assertEqual(self.result['message'],'user sign error')
def test_add_event_success(self):
''' 添加成功'''
payload = {'eid':6,'name':'黑米4手机发布会','limit':2000,'address':'广州白云','start_time':'2017-07-01 12:30:00',
'time':self.client_time,'sign':self.result_md5}
r = requests.post(self.base_url,data=payload)
self.result = r.json()
self.assertEqual(self.result['status'],200)
self.assertEqual(self.result['message'],'add event success')
浙公网安备 33010602011771号