《Web介面開發與自動化測試基於Python語言》--第11章
第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()
對上述程式碼進行分析,見備註。
注意:
encrypt()方法要求被加密的字串長度必須是16、24、32位,如果直接生成可能會引發異常:“ValueError: Input strings must be a multiple of 16 in length”,可是被加密字串的長度是可控的,因為介面引數的個數和長度是不固定的,所以,為了解決這個問題,還需要對字串的長度進行處理,使它的長度符合encrypt()的需求;
通過encrypt()方法加密後的字串是這樣的:
b'>_\x80\xlfi\x97\x8f\x94~\xeaE\……'
- 通過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這種的確相對複雜,書中也只是做了基礎的介紹和應用展示,在實際產品的開發過程中,加密環節相對複雜,測試人員,如果要測試帶有加密的介面,當然了,如果瞭解加密過程最好,如果不瞭解,也不影響測試,只需要通過研發人員獲取到關鍵資訊,加入到介面測試用例中即可。
相關文章
- 《Web介面開發與自動化測試(基於Python語言)》讀書筆記(一)WebPython筆記
- Selenium2自動化測試實戰基於Python語言》讀書筆記--第2章Python筆記
- 《Selenium2自動化測試實戰基於Python語言》讀書筆記--第3章Python筆記
- python介面自動化測試之python基礎語法Python
- 使用go語言開發自動化API測試工具GoAPI
- 基於Selenium+Python的web自動化測試框架PythonWeb框架
- python 介面自動化測試Python
- 基於Python豆瓣自動化測試【2】Python
- python+pytest介面自動化(1)-介面測試基礎Python
- 介面自動化測試錄製工具,讓python selenium自動化測試指令碼開發更加方便Python指令碼
- 基於LangChain手工測試用例轉介面自動化測試生成工具LangChain
- 關於介面測試自動化的總結與思考
- 面向開發的測試技術(三):Web自動化測試Web
- 基於Python的介面自動化-unittest測試框架和ddt資料驅動Python框架
- 基於LangChain手工測試用例轉Web自動化測試生成工具LangChainWeb
- 測試開發之介面篇-使用K6完成介面自動化測試
- 介面自動化測試
- Python 自動化測試開發大綱Python
- 自動化測試-敏捷開發的基礎敏捷
- 關於Web端-UI自動化測試WebUI
- 基於Selenium2 與Python自動化測試環境搭建Python
- Jmeter+Ant+Python 介面自動化測試JMeterPython
- 基於 Pytest+Requests+Allure 實現介面自動化測試
- 測試開發全棧之 Python 自動化全棧Python
- 用Python開發自動化測試指令碼Python指令碼
- 加速Web自動化測試Web
- 測試開發之自動化篇-自動化測試框架設計框架
- 基於 Htte 的 API 自動化測試API
- 關於介面測試——自動化框架的設計與實現框架
- python+pytest介面自動化(11)-測試函式、測試類/測試方法的封裝Python函式封裝
- 介面自動化測試PHPUnit-框架程式碼開發3PHP框架
- 介面自動化測試PHPUnit-框架程式碼開發1PHP框架
- python介面自動化測試之介面資料依賴Python
- 基於 HttpRunner 的介面自動化測試平臺宣講 (已落地)HTTP
- 基於 python--selenium 與 requests 的 web ui/ 介面混合測試框架PythonWebUI框架
- java 自動化與 python 自動化哪種程式語言吃香?JavaPython
- 介面自動化測試框架 HttpFPT框架HTTP
- 二、介面自動化測試(2)