欢迎来到赛兔子家园

接口的安全机制

本篇内容:

  1.  用户认证
  2.  数字签名           

一、用户认证

       在测试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')

posted on 2018-03-23 10:22  赛兔子  阅读(758)  评论(0)    收藏  举报

导航