《Web介面開發與自動化測試基於Python語言》--第11章

二的平方發表於2017-09-26

第11章 介面的安全機制

本章將介紹介面的幾種常用的安全機制。

11.1 使用者認證

介面測試工具的User Auth/Authorization選項,是包含在request請求中的。

11.1.1 開發帶Auth介面

為了練習與安全有關的介面開發,下面重新在sign應用下建立views_if_sec.py檢視檔案:

#! /usr/bin/python
# -*- coding:utf-8 -*-

from django.contrib import auth as django_auth
import base64

# 使用者認證
def user_auth(request):
    get_http_auth = request.META.get('HTTP_AUTHORIZATION', b'')
    auth = get_http_auth.split()
    try:
        auth_parts = base64.b64decode(auth[1]).decode('utf-8').partition(':')
    except IndexError:
        return "null"
    username, password = auth_parts[0], auth_parts[2]
    user = django_auth.authenticate(username=username, password=password)
    if user is not None:
        django_auth.login(request, user)
        return "success"
    else:
        return "fail"

對上述程式碼進行分析:

  • request.META是一個字典,包含了本次HTTP請求的Header資訊,eg:使用者認證、IP地址、使用者Agent(通常是瀏覽器的名稱和版本號)等;HTTP_AUTHORIZATION用於獲取HTTP認證資料,如果為空,將得到一個空的bytes物件

  • 當客戶端傳輸的認證資料為:admin/admin123456,這裡得到的資料為:Basic YWRtaW46YWRtaW4xMjM0NTY=

  • 通過split()方法將其拆分成list,拆分後的資料為:[‘Basic’, ‘YWRtaW46YWRtaW4xMjM0NTY=’]

  • 接下來,取出list中的加密串,通過base64對加密串進行解碼,通過decode()方法以UTF-8編碼對字串進行解碼,partition()方法以冒號“:”為分隔符對字串進行分隔,得到的資料為:(‘admin’, ‘:’, ‘admin123456’)

  • 然後通過try…except…進行異常處理,如果獲取不到Auth資料,則拋IndexError型別的異常,函式返回“null”字串

  • 最後,取出auth_parts元組中對應認證的username和password,最終的資料是:admin、admin123456

  • 最後的最後,呼叫Django的認證模組,對得到的Auth資訊進行驗證,若成功則返回“success”,失敗則返回“fail”

在釋出會查詢介面呼叫上面的user_auth函式:

from django.http import JsonResponse

# 查詢釋出會介面--增加使用者認證
def get_event_list(request):
    auth_result = user_auth(request)    #呼叫使用者認證函式
    if auth_result == "null":
        return JsonResponse({'status':10011, 'message':'user auth null'})

    if 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':'parameter error'})

    if eid != '':
        event = {}
        try:
            result = Event.objects.get(id=eid)
        except ObjectDoesNotExist:
            return JsonResponse({'status':10022, 'message':'query result is empty'})
        else:
            event['name'] = result.name
            event['limit'] = result.limit
            event['status'] = result.status
            event['address'] = result.address
            event['start_time'] = result.start_time
            return JsonResponse({'status':200, 'message':'success', 'data':event})
    if name != '':
        datas = []
        results = Event.objects.filter(name__contains=name)
        if results:
            for r in results:
                event = {}
                event['name'] = r.name
                event['limit'] = r.limit
                event['status'] = r.status
                event['address'] = r.address
                event['start_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'})

這樣就完成了在介面中增加認證機制的功能,只有通過認證才能繼續測試介面全部內容。

11.1.2 介面文件

這裡寫圖片描述

注意: 在urls.py中增加該介面,程式碼如下:

#!/usr/bin/python
# -*- coding:utf-8 -*-

from django.conf.urls import url
from sign import views_if, views_if_sec

urlpatterns = [
    ......
    # ex: /api/get_event_list/
    url(r'^get_event_list', views_if.get_event_list, name='get_event_list'),
    url(r'sec_get_event_list', views_if_sec.get_event_list, name='sec_get_event_list'),
    ......

11.1.3 介面測試用例

編寫對應的測試用例:sec_test_case.py

Ps:Requests庫的get()和post()方法均提供auth引數用於設定使用者簽名。

#!/usr/bin/python
# -*- coding:utf-8 -*-

import unittest
import requests

class GetEventListTest(unittest.TestCase):
    """查詢釋出會資訊帶使用者認證"""

    def setUp(self):
        self.base_url = "http://10.18.214.88:8000/api/sec_get_event_list/"

    def test_get_event_list_auth_null(self):
        """auth為空"""
        r = requests.get(self.base_url, params={'eid':1})
        result = r.json()
        self.assertEqual(result['status'], 10011)
        self.assertEqual(result['message'], 'user auth null')

if __name__ == '__main__':
    unittest.main()

沒有把全部的case寫完,就先試一下效果,結果執行測試,返回的結果卻是錯誤:

E
======================================================================
ERROR: test_get_event_list_auth_null (__main__.GetEventListTest)
auth為空
----------------------------------------------------------------------
Traceback (most recent call last):
  File "sec_test_case.py", line 27, in test_get_event_list_auth_null
    result = r.json()
  File "/usr/local/lib/python2.7/dist-packages/requests/models.py", line 892, in json
    return complexjson.loads(self.text, **kwargs)
  File "/usr/lib/python2.7/dist-packages/simplejson/__init__.py", line 516, in loads
    return _default_decoder.decode(s)
  File "/usr/lib/python2.7/dist-packages/simplejson/decoder.py", line 370, in decode
    obj, end = self.raw_decode(s)
  File "/usr/lib/python2.7/dist-packages/simplejson/decoder.py", line 400, in raw_decode
    return self.scan_once(s, idx=_w(s, idx).end())
JSONDecodeError: Expecting value: line 2 column 1 (char 1)

----------------------------------------------------------------------
Ran 1 test in 0.112s

FAILED (errors=1)

觀察django那邊返回的錯誤,請求的API返回了500錯誤,奇怪哪裡出錯了,但是在測試用例的返回資訊裡,實在是沒看出哪裡有誤啊,不過好在django那的提示資訊比較有用:

Internal Server Error: /api/sec_get_event_list/
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/django/core/handlers/exception.py", line 39, in inner
    response = get_response(request)
  File "/usr/local/lib/python2.7/dist-packages/django/core/handlers/base.py", line 187, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/usr/local/lib/python2.7/dist-packages/django/core/handlers/base.py", line 185, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/home/csg/guest/sign/views_if_sec.py", line 27, in get_event_list
    return JsonResponse({'status':10011, 'message':'user auth null'})
NameError: global name 'JsonResponse' is not defined
[27/Sep/2017 14:05:03] "GET /api/sec_get_event_list/?eid=1 HTTP/1.1" 500 61568

從這裡就很明顯了,原來我們在views_if_sec.py裡沒有定義JsonResponse,好吧我的錯,忘記匯入了,補充到該檔案,重新執行測試用例,成功:

.
----------------------------------------------------------------------
Ran 1 test in 0.016s

OK

將測試用例補充完整即可:

#!/usr/bin/python
# -*- coding:utf-8 -*-

#########################################################
# (C) 2000-2017 NSFOCUS Corporation. All rights Reserved#
#########################################################

"""
Function:
Create Time: 2017年09月26日 星期二 22時50分33秒
Author: zhaoxinzhen@intra.nsfocus.com
"""


import unittest
import requests

class GetEventListTest(unittest.TestCase):
    """查詢釋出會資訊帶使用者認證"""

    def setUp(self):
        self.base_url = "http://10.18.214.88:8000/api/sec_get_event_list/"

    def test_get_event_list_auth_null(self):
        """auth為空"""
        r = requests.get(self.base_url, params={'eid':1})
        result = r.json()
        self.assertEqual(result['status'], 10011)
        self.assertEqual(result['message'], 'user auth null')

    def test_get_event_list_auth_error(self):
        """auth錯誤"""
        auth_user = ('abc', '123')
        r = requests.get(self.base_url, auth=auth_user, params={'eid':1})
        result = r.json()
        self.assertEqual(result['status'], 10012)
        self.assertEqual(result['message'], 'user auth fail')

    def test_get_event_list_eid_null(self):
        """eid引數為空"""
        auth_user = ('admin', 'admin123456')
        r = requests.get(self.base_url, auth=auth_user, params={'eid':''})
        result = r.json()
        self.assertEqual(result['status'], 10021)
        self.assertEqual(result['message'], 'parameter error')

    def test_get_event_list_eid_success(self):
        """根據eid查詢結果成功"""
        auth_user = ('admin', 'admin123456')
        r = requests.get(self.base_url, auth=auth_user, params={'eid':1})
        result = r.json()
        self.assertEqual(result['status'], 200)
        self.assertEqual(result['message'], 'success')
        self.assertEqual(result['data']['name'], u'小米5釋出會')
        self.assertEqual(result['data']['address'], u'北京國家會議中心')

if __name__ == '__main__':
    unittest.main()

11.2 數字簽名

在使用HTTP/SOAP協議傳輸資料時,簽名作為其中一個引數,有著重要的作用:

  • 鑑權: 通過客戶端的金鑰和服務端的金鑰匹配。

如何理解鑑權呢,舉個例子:

向介面傳參:http://127.0.0.1:8000/api/?a=1&b=2

假設簽名的金鑰為:@admin123

加上簽名之後的介面傳參為:http://127.0.0.1:8000/api/?a=1&b=2&sign=@admin123

顯然這樣明文傳輸sign引數是不安全的,一般會通過加密演算法進行加密,eg:MD5

>>> import hashlib
>>> md5 = hashlib.md5()
>>> sign_str = "@admin123"
>>> sign_bytes_utf8 = sign_str.encode(encoding="utf-8")
>>> md5.update(sign_bytes_utf8)
>>> md5.hexdigest()
'4b9db269c5f978e1264480b0a7619eea'
>>> 

似曾相識啊,目前正在測試的第三方應用介面就是使用的這種數字簽名方式。

將“@admin123”通過MD5加密之後得到:4b9db269c5f978e1264480b0a7619eea

單獨作為鑑權,帶簽名的介面為:http://127.0.0.1:8000/api/?a=1&b=2&sign=4b9db269c5f978e1264480b0a7619eea

使用MD5加密演算法,好處是不可逆,當伺服器接收到引數後,同樣需要對“@admin123”進行MD5加密,然後與呼叫者傳來的sign加密字串對比是否一致,從而來鑑別呼叫者是否有權訪問介面。

MD5 Message-Digest Algorithm 5 訊息摘要加密演算法第五版,用於確保資訊傳輸的完整一致,是計算機廣泛使用的雜湊演算法之一,主流程式語言已經普遍支援MD5實現。

  • 資料防篡改: 引數是明文傳輸,將介面引數及金鑰生成加密字串,將加密字串作為簽名。

eg:http://127.0.0.1:8000/api/?a=1&b=2

假設簽名的金鑰為:@admin123

簽名的明文為:a=1&b=2&api_key=@admin123

通過MD5演算法將整個介面引數(a=1&b=2&api_key=@admin123)生成加密字串:786bfe32ae1d3764f208e03ca0bfaf13

所以,作為資料防篡改,帶簽名的介面為:

http://127.0.0.1:8000/api/?a=1&b=2&sign=786bfe32ae1d3764f208e03ca0bfaf13

對整個介面的引數做了加密,所以,只要任意一個引數發生變化,簽名的驗證就會失敗。

好處是:加強了鑑權和對資料完整性的保護;

壞處是:MD5加密不可逆,伺服器端必須知道客戶端的介面引數和值,否則簽名的驗證就會失敗。但實際上介面在設計時,伺服器端是不知道客戶端的請求引數值的。eg:嘉賓手機號的查詢,伺服器不知道呼叫者傳的手機號具體是什麼,只是通過資料庫來查詢該號碼是否存在,那麼就不能使用全引數加密的方式。

11.2.1 開發介面

編輯:…/guest/sign/views_if_sec.py檢視檔案

import time, hashlib

# 使用者簽名+時間戳
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"

    # 簽名檢查
    md5 = hashlib.md5()
    sign_str = client_time + "&Guest-Bugmaster"
    sign_bytes_utf8 = sign_str.encode(encoding="utf-8")
    md5.update(sign_bytes_utf8)
    server_sign = md5.hexdigest()

    if server_sign != client_sign:
        return "sign fail"
    else:
        return "sign success"

對上述程式碼進行分析:

  • 建立user_sign()函式,處理簽名引數;

  • 首先,通過POST方法獲取兩個引數client_time和client_sign,如果客戶端請求方法不是POST,那麼函式返回error錯誤;

  • 然後,判斷兩個引數均不能為空,如果為空,則返回sign null錯誤;

  • 然後,對時間戳進行判斷,對客戶端的時間戳和伺服器端的時間戳進行判斷,如果時間戳大於60,則返回超時timeout錯誤;

  • 最後,對簽名進行檢查,如果簽名檢查通過,則返回成功,否則返回簽名驗證失敗。

注意: 之所以將時間戳用“.”split,是因為python3的時間戳精度較高,但我們只需要小數點的前10位。

將使用者簽名功能應用到新增釋出會介面中,編輯:…/guest/sign/views_if_sec.py檢視檔案:

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':10022, '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', '')
    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 ValidationError 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'})

呼叫user_sign()函式處理使用者簽名,根據函式返回字串,將相應的處理結果返回給客戶端,當使用者簽名驗證通過後,接下來的處理過程和之前是一樣的。

11.2.2 介面文件

這裡寫圖片描述

11.2.3 介面用例

由於介面中加入了時間戳和md5加密演算法,所以一般的介面測試工具無法模擬,此時就凸顯出通過程式碼方式測試介面的優越性了。

新增介面測試用例:add_event_test.py

#!/usr/bin/python
# -*- coding:utf-8 -*-


import unittest, requests, hashlib
from time import time


class AddEventTest(unittest.TestCase):

    def setUp(self):
        self.base_url = "http://127.0.0.1:8000/api/sec_add_event/"
        # app_key
        self.api_key = "&Guest-Bugmaster"
        # 當前時間
        now_time = time()
        self.client_time = str(now_time).split('.')[0]
        # sign
        md5 = hashlib.md5()
        sign_str = self.client_time + self.api_key
        sign_bytes_utf8 = sign_str.encode(encoding="utf-8")
        md5.update(sign_bytes_utf8)
        self.sign_md5 = md5.hexdigest()

    def test_add_event_request_error(self):
        '''請求方法錯誤'''
        r = requests.get(self.base_url)
        result = r.json()
        self.assertEqual(result['status']. 10011)
        self.assertEqual(result['message'], 'request error')

    def test_add_event_sign_null(self):
        '''簽名引數為空'''
        payload = {'eid':1, 'limit':'', 'address':'', 'start_time':'',
        'time':'', 'sign':''}
        r = requests.post(self.base_url, data=payload)
        result = r.json()
        self.assertEqual(result['status'], 10012)
        self.assertEqual(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)
        result = r.json()
        self.assertEqual(result['status'], 10013)
        self.assertEqual(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.assertEqual(result['status'], 10014)
        self.assertEqual(result['message'], 'user sign error')

    def test_add_event_success(self):
        '''新增成功'''
        payload = {'eid':21, 'name':'一加5手機釋出會', 'limit':2000,
        'address':'深圳寶體', 'start_time':'2017-05-10 12:00:00',
        'time':self.client_time, 'sign':self.sign_md5}
        r = requests.post(self.base_url, data=payload)
        result = r.json()
        self.assertEqual(result['status'], 200)
        self.assertEqual(result['message'], 'add event success')


if __name__ == '__main__':
    unittest.main()

11.3 介面加密

以AES加密演算法為例。

11.3.1 PyCrypto庫

PyCrypto庫,是一個免費的加密演算法庫,支援常見的DES、AES加密,以及MD5、SHA等各種HASH運算。

PyCrypto在Windows系統中安裝需要依賴於“vcvarsall.bat”檔案,需要安裝龐大的Visual Studio才能解決,所以建議還是使用Linux系統來進行學習和使用。

通過下面的例子來演示PyCrypto庫的強大:

  • eg1

SHA-256演算法屬於密碼SHA-2系列雜湊,它產生了一個訊息的256位摘要。雜湊值用作表示大量資料的固定大小的唯一值,資料的少量更改會在雜湊值中產生不可預知的大量更改,從而驗證資料的安全。

>>> from Crypto.Hash import SHA256
>>> hash = SHA256.new()
>>> hash.update(b'message')
>>> hash.digest()    #使用digest()方法加密
'\xabS\n\x13\xe4Y\x14\x98+y\xf9\xb7\xe3\xfb\xa9\x94\xcf\xd1\xf3\xfb"\xf7\x1c\xea\x1a\xfb\xf0+F\x0cm\x1d'
>>> hash.hexdigest()    #使用hexdigest()方法加密
'ab530a13e45914982b79f9b7e3fba994cfd1f3fb22f71cea1afbf02b460c6d1d'
>>> 

通過digest()方法可以對字串“message”進行加密,當然,通過hexdigest()方法也可以將其轉換為16進位制的加密字串。

  • eg2

AES是Advanced Encryption Standard的縮寫,即高階加密標準,是目前非常流行的加密演算法。

>>> from Crypto.Cipher import AES
>>> obj = AES.new('This is a key123', AES.MODE_CBC, 'This is an IV456')
>>> message = "The answer is no"
>>> ciphertext = obj.encrypt(message)    #加密
>>> ciphertext
'\xd6\x83\x8dd!VT\x92\xaa`A\x05\xe0\x9b\x8b\xf1'
>>> obj2 = AES.new('This is a key123', AES.MODE_CBC, 'This is an IV456')
>>> obj2.decrypt(ciphertext)    #解密
'The answer is no'
>>> 

加密的過程:

  • “This is a key123”為key,長度有著嚴格的要求,必須為16、24或32位,否則會丟擲異常:“ValueError: AES key must be either 16, 24 or 32 bytes long”。

  • “This is an IV456”為VI,長度要求更加嚴格,只能為16位,否則將丟擲異常:“ValueError: IV must be 16 bytes long”。

  • 通過encrypt()方法對message字串進行加密,得到:’\xd6\x83\x8dd!VT\x92\xaa`A\x05\xe0\x9b\x8b\xf1’

解密的過程:

  • 要想對加密字串進行解密,則必須知道加密時所使用的key和VI,通過decrypt()方法對加密字串解密得到:“The answer is no”。

  • eg3

此外,Crypto還提供了一個強大的隨機演算法。

>>> from Crypto.Random import random
>>> random.choice(['dogs', 'cats', 'bears'])
'bears'
>>> 

11.3.2 AES加密介面開發

將AES加密演算法應用到介面開發中,先從編寫測試用例開始,因為加密的過程是在客戶端進行的,也就是在測試用例當中進行。

編寫介面測試用例檔案:Interface_AES_test.py

#! /usr/bin/python
# -*- coding:utf-8 -*-

from Crypto.Cipher import AES
import base64
import requests
import unittest
import json

class AESTest(unittest.TestCase):
    """AES加密後的介面測試用例"""

    def setUp(self):
        """初始化測試引數"""
        BS = 16
        self.pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)    # 使用lambda定義匿名函式來對字串進行補足,使其長度變為16、24、32位
        self.base_url = "http://127.0.0.1:8000/api/sec_get_guest_list/"
        self.app_key = "W7v4D60fds2Cmk2U"    # 定義好app_key,app_key是金鑰,只能告訴給合法的介面呼叫者

    def encryptBase64(self, src):
        return base64.urlsafe_b64encode(src)

    def encryptAES(self, src, key):
        """生成AES密文"""
        iv = b"1172311105789011"    # 定義好iv,iv也是保密的,必須為16位
        cryptor = AES.new(key, AES.MODE_CBC, iv)
        ciphertext = cryptor.encrypt(self.pad(src))    # 通過encrypt()方法對src(JSON格式的介面引數)生成加密字串
        return self.encryptBase64(ciphertext)    # 通過encrypt()方法生成的加密字串太長不適合傳輸,於是,通過base64模組的urlsafe_b64encode()方法對AES加密字串進行二次加密

    def test_case_interface(self):
        """測試AES加密的介面"""
        payload = {'eid':1, 'phone': '18011001100'}    # 使用字典格式來存放介面引數
        # 加密過程
        encoded = self.encryptAES(json.dumps(payload), self.app_key).decode()    # 通過json.dumps()方法將payload字典轉化為JSON格式,和app_key一起做為encryptAES()方法的引數,用於生成AES加密字串

        r = requests.post(self.base_url, data = {"data": encoded})    # 將加密後的字串作為data引數傳送到介面請求
        result = r.json()
        self.assertEqual(result['status'], 200)
        self.assertEqual(result['message'], 'success')

if __name__ == '__main__':
    unittest.main()

對上述程式碼進行分析,見備註。

注意:

  1. encrypt()方法要求被加密的字串長度必須是16、24、32位,如果直接生成可能會引發異常:“ValueError: Input strings must be a multiple of 16 in length”,可是被加密字串的長度是可控的,因為介面引數的個數和長度是不固定的,所以,為了解決這個問題,還需要對字串的長度進行處理,使它的長度符合encrypt()的需求;

  2. 通過encrypt()方法加密後的字串是這樣的:

b'>_\x80\xlfi\x97\x8f\x94~\xeaE\……'
  1. 通過urlsafe_b64encode()方法加密後的字串是這樣的:
b'gouBbuKWEeY5w……'

當伺服器接收到加密的介面引數後,需要再警告一系列的過程解密:

編輯介面檔案:views_if_sec.py

#! /usr/bin/python
# -*- coding:utf-8 -*-

import json
from Crypto.Cipher import AES


# AES加密演算法
BS = 16
unpad = lambda s: s[0: - ord(s[-1])]

def decryptBase64(src):
    return base64.urlsafe_b64decode(src)    # 通過urlsafe_b64decode()方法對base64加密字串進行解密

def decryptAES(src, key):
    """
    解析AES密文
    """
    src = decryptBase64(src)    # 呼叫decryptBase64()方法,將base64加密字串解密為AES加密字串
    iv = b'1172311105789011'
    cryptor = AES.new(key, AES.MODE_CBC, iv)
    text = cryptor.decrypt(src).decode()    # 通過decrypt()對AES加密字串進行解密
    return unpad(text)    # 通過unpad匿名函式對字串的長度進行還原

def aes_encryption(request):

    app_key = 'W7v4D60fds2Cmk2U'    # 伺服器端與合法客戶端約定的金鑰app_key

    if request.method == "POST":    # 判斷客戶端請求方法是否為POST,通過POST.get()方法接收data引數
        data = request.POST.get("data", "")
    else:
        return "error"    # 如果請求方法不為POST,則函式返回“error”字串

    # 解密
    decode = decryptAES(data, app_key)    # 呼叫decryptAES()函式解密,傳引數字串和app_key
    # 轉化為字典
    dict_data = json.loads(decode)    # 將解密後的字串通過json.loads()方法轉化成字典,並作為函式的返回值
    return dict_data

在查詢嘉賓列表的介面中呼叫aes_encryption()函式進行AES加密字串的解密,繼續編輯views_if_sec.py檔案:

# 嘉賓查詢介面---AES演算法
def get_guest_list(request):
    dict_data = aes_encryption(request)

    if dict_data == "error":
        return JsonResponse({'status':10011, 'message':'request error'})

    # 取出對應的釋出會id和嘉賓手機號
    eid = dict_data['eid']
    phone = dict_data['phone']

    if eid == '':
        return JsonResponse({'status':10021, 'message':'eid cannot be empty'})

    if eid != '' and phone == '':
        datas = []
        results = Guest.objects.filter(event_id=eid)
        if results:
            for r in results:
                guest = {}
                guest['realname'] = r.realname
                guest['phone'] = r.phone
                guest['email'] = r.email
                guest['sign'] = r.sign
                datas.append(guest)
            return JsonResponse({'status':200, 'message':'success', 'data':datas})
        else:
            return JsonResponse({'status':10022, 'message':'query result is empty'})

    if eid != '' and phone != '':
        guest = {}
        try:
            result = Guest.objects.get(phone=phone, event_id=eid)
        except ObjectDoesNotExist:
            return JsonResponse({'status':10022, 'message':'query result is empty'})
        else:
            guest['realname'] = result.realname
            guest['phone'] = result.phone
            guest['email'] = result.email
            guest['sign'] = result.sign
            return JsonResponse({'status':200, 'message':'success', 'data':guest})

如果aes_encryption()函式返回“error”,則說明該介面的方法呼叫錯誤,返回客戶端“request error”,否則,取出解密字串(字典)中的eid 和phone的引數進行查詢嘉賓列表的處理。

11.3.3 介面文件

增加了加密後的查詢嘉賓介面文件:

這裡寫圖片描述

11.3.4 補充介面測試用例

最後,補充查詢嘉賓介面的測試用例:

Interface_AES_test.py

#! /usr/bin/python
# -*- coding:utf-8 -*-

from Crypto.Cipher import AES
import base64
import requests
import unittest
import json

class AESTest(unittest.TestCase):
    """AES加密後的介面測試用例"""

    def setUp(self):
        """初始化測試引數"""
        BS = 16
        self.pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
        self.base_url = "http://10.18.214.88:8000/api/sec_get_guest_list/"
        self.app_key = "W7v4D60fds2Cmk2U"

    def encryptBase64(self, src):
        return base64.urlsafe_b64encode(src)

    def encryptAES(self, src, key):
        """生成AES密文"""
        iv = b"1172311105789011"
        cryptor = AES.new(key, AES.MODE_CBC, iv)
        ciphertext = cryptor.encrypt(self.pad(src))
        return self.encryptBase64(ciphertext)

    def test_case_interface(self):
        """測試AES加密的介面"""
        payload = {'eid':1, 'phone': '18011001100'}
        # 加密
        encoded = self.encryptAES(json.dumps(payload), self.app_key).decode()

        r = requests.post(self.base_url, data = {"data": encoded})
        result = r.json()
        self.assertEqual(result['status'], 200)
        self.assertEqual(result['message'], 'success')

    def test_get_guest_list_request_error(self):
        """測試嘉賓查詢介面:eid為空"""
        payload = {'eid': '', 'phone': ''}
        encoded = self.encryptAES(json.dumps(payload), self.app_key).decode()
        r = requests.post(self.base_url, data={"data": encoded})
        result = r.json()
        self.assertEqual(result['status'], 10011)
        self.assertEqual(result['message'], 'request error')

    def test_get_guest_list_eid_null(self):
        """測試嘉賓查詢介面:phone為空"""
        payload = {'eid': '', 'phone': '18011001100'}
        encoded = self.encryptAES(json.dumps(payload), self.app_key).decode()
        r = requests.post(self.base_url, data={"data": encoded})
        result = r.json()
        self.assertEqual(result['status'], 10021)
        self.assertEqual(result['message'], 'eid cannot be empty')

    def test_get_guest_list_eid_error(self):
        """根據eid查詢結果為空"""
        payload = {'eid': '901', 'phone': ''}
        encoded = self.encryptAES(json.dumps(payload), self.app_key).decode()
        r = requests.post(self.base_url, data={"data": encoded})
        result = r.json()
        self.assertEqual(result['status'], 10022)
        self.assertEqual(result['message'], 'query result is empty')

    def test_get_guest_list_eid_success(self):
        """根據eid查詢結果成功"""
        payload = {'eid': '1', 'phone': '18011001100'}
        encoded = self.encryptAES(json.dumps(payload), self.app_key).decode()
        r = requests.post(self.base_url, data={"data": encoded})
        result = r.json()
        self.assertEqual(result['status'], 200)
        self.assertEqual(result['message'], 'success')
        self.assertEqual(result['data'][0]['realname'], 'alen')
        self.assertEqual(result['data'][0]['phone'], '18011001100')

    def test_get_event_list_eid_phone_null(self):
        """根據eid和phone查詢結果為空"""
        payload = {'eid': '200', 'phone': '10000000000'}
        encoded = self.encryptAES(json.dumps(payload), self.app_key).decode()
        r = requests.post(self.base_url, data={"data": encoded})
        result = r.json()
        self.assertEqual(result['status'], 10022)
        self.assertEqual(result['message'], 'query result is empty')

    def test_get_event_list_eid_phone_success(self):
        """根據eid和phone查詢結果成功"""
        payload = {'eid': '1', 'phone': '18011001100'}
        encoded = self.encryptAES(json.dumps(payload), self.app_key).decode()
        r = requests.post(self.base_url, data={"data": encoded})
        result = r.json()
        self.assertEqual(result['status'], 200)
        self.assertEqual(result['message'], 'success')
        self.assertEqual(result['data']['realname'], 'alen')
        self.assertEqual(result['data']['phone'], '18011001100')    

if __name__ == '__main__':
    unittest.main()

封裝了AES演算法的加密演算法後,在介面測試用例中呼叫即可,過程不復雜。

總結

使用MD5方式的相對來說還簡單點,AES這種的確相對複雜,書中也只是做了基礎的介紹和應用展示,在實際產品的開發過程中,加密環節相對複雜,測試人員,如果要測試帶有加密的介面,當然了,如果瞭解加密過程最好,如果不瞭解,也不影響測試,只需要通過研發人員獲取到關鍵資訊,加入到介面測試用例中即可。

相關文章