接口的安全机制

438 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第24天,点击查看活动详情

用户认证

在我们测试web接口时,不管所用的工具还是Requests库都提供的Auth的选项,这个选项同样需要填 写username 和password,但这里Auth的用户名和密码与系统登录的用户名密码有区别,因为它不是作为 GET/POST请求的参数发送的。

通过Postman 填写Auth选项。

image.png

通过Fiddler 工具抓取请求。

image.png

其实,这个问题难点并不再测试上面。你是否和我一样好奇,如何通过Django开发一个可以处理Auth 的接口。为此我翻了很久的Django文档,然而并没有找到想要的结果。Django—REST—framwork框架(后面章 节会介绍该框架的使用)自带的有这样的一个Auth的功能,在接口调用的时候需要填写Auth认证。通过查 看 Django-REST-framwork框架的源码,找到了答案。

开发带Auth接口

相信学到这里关于Django开发已经很熟悉了,为了练习与安全有关的接口开发,重新创 建.../sign/views_if_sec.py视图文件。

接口的处理逻辑主要由 views 层完成。所以,这里只提供views层的实现。

import base64
from django.contrib import auth as django_auth
import hashlib

#用户认证

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('iso-8859-1').partition(':')

    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"

代码并不算太多,但这里面包含的知识点不少,我们需要逐一的进行解释。

get_http_auth =request.META.get('HTTTTP_AUTHORIZATION",b")

request.META是一个Python字典,包含了所有本次HTTP请求的Header信息,比如用户认证、IP地址 和用户Agent(通常是浏览器的名称和版本号)等。

HTTP_AUTHORIZATION 用于获取 HTTP authorization。

然后,得到的数据是这样的:Basic YWRtaW46YWRtaW4xMjMONTY=

auth = get_th.split() 通过split(方法将其拆分成list。拆分后的数据是这样的: ['Basic','YWRtaW46YWRtaW4xMjMONTY=]

autth_parts=base64.b64decode(auth[1]).decode('iso-8859-1').partition(':')

取出list中的加密串,通过 base64对加密串进行解码。得到的数据是:(admin',,'admin123456)

执行到这一行,如果获取不到Auth信息,将会抛IndexError异常,通过try...except...进行异常捕捉,如 果捕捉到异常将返回“null”。

userid,,password=auth_parts[0],auth_parts[2]

最后,取出元组中对应的用户id和密码。最终于数据:admin admin123456

再接来的处理过程我们就很熟悉了。调用Django的认证模块,对得到Auth信息进行认证。成功将返回 “success”,失败则返回“fail”。

需要说明的是,这种认证方式是一种弱认证,安全性较低。 最后,将其应用到发布会查询接口中。

#发布会查询接口———增加用户认证
#
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", "")  # 发布会名称

在../sign/urls.py文件中添加新的安全接口指向。

from sign import views_if,views_if_security


urlpatterns=[
    ...



# security interface:

# ex:/sign/sec_get_event_list/

    url(r'^sec_get_event_list/', views_if_security.get_event_list, name='get_event_list'), [

]

编写接口文档

发布会查询接口文档:

image.png

image.png

测试接口

import unittest

import requests

class GetEventListTest(unittest.TestCase):

    """ 查询发布会信息(带用户认证)"""

    def setUp(self):

        self.base_url = "http://127.0.0.1: 8000 / sign / sec_get_event_list / "

        self.auth_user = ('admin','in123456')

    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 nu11')

    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.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查询结果成功"""

        r = requests.get(self.base_url, auth=self.auth_user, params = {'eid': 1})

        result = r.json()

        self.assertEqual(result['status'], 200)

        self.assertEqual(result['message'], 'success')

        self.assertEqual(result["data"],["name"],u'mx6发布会')

        self.assertEqual(result["data"],['address'],u'北京国家会议中心')

数字签名

在使用HTTP/SOAP协议传输数据的时候,签名作为其中一个参数,可以起到关键作用:一、鉴权:通过客户的密钥,服务端的密钥匹配;

这个很有好理解,例如一个接口传参为:http:/ttp://127.0.0.1:8000/sign/?a=1&b=2

假设签名的密钥为:

@adm 加上签名之后的接口参数为:

ttp://127.0.0.1:8000/sign/?a=1&b=2&sign=@admin123 http

显然,sign参数明文传输是不安全的,所以,一般会通过加密算法进行加密

import hashlib

md5=hashlib.md5

sign_str="@adr@admin123"

sign_bytes_utf8 = sign_str.encode(encoding="utf-8")

md5.update(sign_bytes_utf8)

sign_md5 = md5.hexdigest() 

print(sign_md5)

执行程序将会得到:4b9db269c5f978e1264480b0a7619eea 所以,单做为鉴权,带签名的接口为:

http://127.0.0.1:8000/sign/?a=1&b=2&sign=4b9db2

db269c5f978e1264480b0a7619eea

因为MD5算法是不可逆向的,所以,当服务器接收到请求后,同样需要对“@admin123”进行MD5加密,然后,比较客户端传来的sign加密串是否与服务器端生成的密码串一致。

二、数据防篡改:参数是明文传输,将参数及密钥加密作为签名与服务器匹配;同样是这样一个带参数的接口:

ttp://127.0.0.1:8000/sign/?a=1&b=2 加密方式比前者要复杂。

假设签名的密钥为:@admin123签名的明文为:

a=1&b=2&api_key=@admin123

再次通过上面的代码对整个接参与值生成MD5加密串:786bfe32aeld3764f208e03ca0bfaf13所以,带参数的接口串为:

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

2aeld3764f208e03ca0bfaf13

因为整个接口的参数做了加密,所以,只要任意一个参数发改变,那签名验证就会失败。从而起到了鉴权及数据完整性的保护。

不过,接口全参数的加密签名也存在弊端,因为MD5加密是不可逆的,所以,服务器端必须已知客户端的接口参数和值,否则签名的验证就会失败。但一般接口在设计时对客户端所请求的参数并完全已知,例如,嘉宾手机号查询,服务器并不知道客户传的手机号是什么,需要根据数据库的查询结果来做出相应的返回值。