API 接口认证与传输数据加密

应用场景

cmdb 这类项目的资产入库等操作,当agent 与server 端通过api 进行数据交互时,为了安全采取了两项安全措施:1、server 端需要对agent 端进行身份验证(避免有冒充agent 请况);2、当agent 采集数据提交到server 端过程需要对数据加密,数据传输到agent 端时再数据解密(防止数据传输过程被截取,泄露数据)。

api 身份认证

认证原理

server 与 agent保存相同的字符串密钥,agent 通过密钥生成签名值,把数据与签名值发送给server 端,server 根据自己的配置文件保存的密钥生成签名值与agent 端签名值比对,两者一致则认证通过否则失败。

安全加强(只针对数据传输中签名被截取安全隐患,暂不考虑server与agent 本地保存的字符串密钥安全隐患)

签名值一次性

agent 每次发送过来的签名值都会被server 记录,每次发送的签名值如果已经使用过则认证失败(防止传输中被截取后黑客可以永久使用被截取的签名密钥来认证)

动态生成签名

由于“签名值一次性”所以签名密钥必须动态生成,每次用不同的密钥。agent 每次生成签名前生成一个时间值,把时间与保存的字符串密钥组合在一起再生成一个签名值。签名值变成了动态。

签名超时设置

agent 每次发送签名与生成签名的时间到server ,server 端接收时会再本段生成一个时间,将两个时间值比较,当时间差超过10s ,则认证失败。(“签名值一次性”中对认证过的签名值记录在了字典中以{"client_time":"签名密钥"}形式存储,为了避免此字典无线增大占用内存,所以会对字典中的时间判断,超过10s 的数据进行删除,但是删除的签名数据再有相同的签名数据来认证时就不能通过“签名一次性”来排除了,所以“签名超时设置”解决了此问题)

 代码

agent 端

import requests
import time
import hashlib


def gen_sign(ctime):
    key = "uiakjsdfasjdf898"                                 #此字符串在server端也保存一份   
    val = "%s|%s" %(key,ctime)                           #加入时间,动态生成签名密钥
    obj = hashlib.md5()                                        #生成一个对象
    obj.update(val.encode('utf-8'))                        #md5只能对字节形式的数据进行加密
    return obj.hexdigest()

ctime = int(time.time()*1000)                              #*1000单位变ms,int 转化为整数(去掉小数点后的数字,而不是四舍五入)
result = requests.post(
    url = 'http://127.0.0.1:8000/api/test/',              #注意路径结尾一定更要以/结尾
    params = {'sign':gen_sign(ctime),'ctime':ctime} #相当于在url = 'http://.../?key="alskdjflskdfjkjk"',也就是在url中以get形式发送数据
)
print(result.url,result.text)

#认证逻辑:agent 把签名密钥与生成签名密钥的时间一并发送到server端,server 端通过自身保存的key与agent 的时间生成签名值,两个签名比对进行认证
View Code

server 端(view.py)

import json
import hashlib                                    #此模块里有md5的类
import time
from django.shortcuts import render,HttpResponse
from django.views import View
from django.conf import settings
from rest_framework.views import APIView
from rest_framework.response import Response

#生成签名的函数
def gen_sign(ctime):
    """
    生成签名
    :param ctime:
    :return:
    """
    val = '%s|%s' %(settings.URL_AUTH_KEY,ctime,)
    obj = hashlib.md5()                                #实力化一个md5对象
    obj.update(val.encode('utf-8'))                    #md5对象只能对bytes格式数据加密
    return obj.hexdigest()                            #返回生成的签名值
    
#所有的验证成功的签名密钥都会记录在SIGN_RECORD 中
SIGN_RECORD = {}

class TestView(APIView):
    def post(self,request):
        print("请求来了")
        #由于继承了APIView所以此处的request 不是django原生的request,原生的为request._request
        client_sign = request._request.GET.get('sign')              #获取agent发来的签名密钥
        client_ctime = int(request._request.GET.get('ctime'))       #获取agent 发来的时间
        server_time = int(time.time() * 1000)                       #记录当前时间,*1000单位变成ms
        
        #认证第一关
        if server_time - client_ctime > 5000:                       #比对两个时间,超过5s,则认证失败
            return Response({'status': 'false', 'error': "路上时间太久了"})
        # 认证第二关
        if client_sign in SIGN_RECORD:                              #已经认证通过的签名会保存在SING_RECORD,新来密钥如果被认证过了就会认证失败
            return Response("签名已经被使用过了")
        #认证第三关
        server_sign = gen_sign(client_ctime)                        #server 端生成签名
        if client_sign != server_sign:                              #密钥比对来通过认证
            return Response({'status':'false','error':403})
        #认证通过进行以下操作
        SIGN_RECORD[client_sign] = client_ctime                     #通过认证的密钥存储起来,下次防止密钥重复使用

        #防止SIGN_RECORD无线增大,对于5s 之前的记录删除。对于已经认证通过并存储在该记录中的密钥,超过10s删除后,
        ## 那么认证第二关就失效了,所以认证第一关就是为此而设
        for k in list(SIGN_RECORD.keys()):
            v = SIGN_RECORD[k]
            if server_time - v  > 5000:
                print("已经超过5s")
                del SIGN_RECORD[k]
        return Response({'status':'true','data':666})
View Code

数据加密

加密原理

通过rsa 模块生成公钥和私钥分别保存在agent 与server 端,agent 通过公钥加密数据传输到server 端通过私钥解密。rsa 加密分为1024与2048两种,生成公私钥对象时指定参数即可 pub_key_obj, priv_key_obj = rsa.newkeys(1024) ,1024指的是能加密的数据的位数,换算成字节就是1024/8=128 bytes

代码

生成公私钥,分别保存在server/agent 两端

# ######### 1. 生成公钥私钥 #########
pub_key_obj, priv_key_obj = rsa.newkeys(1024)                 # 1024/8 = 128 ,128 - 11 = 117
# 公钥字符串
pub_key_str = pub_key_obj.save_pkcs1()
pub_key_code = base64.standard_b64encode(pub_key_str)

# 私钥字符串
priv_key_str = priv_key_obj.save_pkcs1()
priv_key_code = base64.standard_b64encode(priv_key_str)

将生成的公私钥(经过base64.standard_b64encode处理的)分别保存在agent/server 配置文件settings.py中

原生的公钥:
    b'-----BEGIN RSA PUBLIC KEY-----\nMIGJAoGBAJo2DEaukeIBTvc5vscIrh0gU79N+XRrf6NBGxGi6eOh7muzH3VV7UIn\nZvfUE3Nxu97DiMAC1u2JEudM8iatMChSLxSh9qFNB36ejz7dCi9DrAH6Ce46JZ7h\n+iwlo9x7Qr4uLJrQsHhia4/i89aAooNV6I8Ne+WOe3V1PvEUs+PhAgMBAAE=\n-----END RSA PUBLIC KEY-----\n'
base64.standard_b64encode编码处理后:
    b'LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JR0pBb0dCQUpvMkRFYXVrZUlCVHZjNXZzY0lyaDBnVTc5TitYUnJmNk5CR3hHaTZlT2g3bXV6SDNWVjdVSW4KWnZmVUUzTnh1OTdEaU1BQzF1MkpFdWRNOGlhdE1DaFNMeFNoOXFGTkIzNmVqejdkQ2k5RHJBSDZDZTQ2Slo3aAoraXdsbzl4N1FyNHVMSnJRc0hoaWE0L2k4OWFBb29OVjZJOE5lK1dPZTNWMVB2RVVzK1BoQWdNQkFBRT0KLS0tLS1FTkQgUlNBIFBVQkxJQyBLRVktLS0tLQo='
View Code

agent 端

import rsa
import base64
from config import settings
#数据加密函数
def encrypt(value_bytes):
    """
    rsa 公钥加密
    :param value_bytes: 要加密的字节
    :return:
    """
    key_str = base64.standard_b64decode(settings.PUB_KEY)    #settings.py 中记录了公钥
    pk = rsa.PublicKey.load_pkcs1(key_str)
    #rsa 1024 能加密的数据大小为128(1024/8)字节,rsa 本身数据占用11字节,所以对大于128字节的数据分批次加密(每次加密117(128-11)字节)最后加密的数据拼接即可。
    data_list = []
    for i in range(0,len(value_bytes),117):
        chunk = value_bytes[i:i+117]
        result = rsa.encrypt(chunk, pk)
        data_list.append(result)

    return b''.join(data_list)

#进行数据加密,并且发送到agent
ctime = int(time.time()*1000)
r1 = requests.post(
     url=self.asset_api,
     params = {'sign':gen_sign(ctime),'ctime':ctime},         #传输签名密钥
     data=encrypt(json.dumps(info).encode('utf-8')),          #传输数据rsa 加密
     headers={'Content-Type':'application/json'}
    )
View Code

server 端

#使用私钥解密的函数
import rsa
import base64
def decrypt(bytes_value):
    """
    rsa解密
    :param bytes_value: 要解密的数据为bytes 类型,因为rsa 只能对bytes 类型数据加密,所以解密数据为bytes类型
    :return: 解密完成的字节
    """
    key_str = base64.standard_b64decode(settings.PRIV_KEY)    #生成密钥后使用base64.standard_b64encode对公钥进行了编码处理,所以此处要用decode 解码
    pk = rsa.PrivateKey.load_pkcs1(key_str)                    #生成私钥证书    
    #rsa 1024 能加密的数据大小为128(1024/8)字节,所以需要对大于128字节的数据分批次加密(每次加密128字节)最后加密的数据拼接即可。所以解密时也是每次解密128字节,最后拼接即可
    result = []
    for i in range(0,len(bytes_value),128):
        chunk = bytes_value[i:i+128]                        
        val = rsa.decrypt(chunk, pk)                        #通过公钥证书对数据chunk 加密
        result.append(val)
    return b''.join(result)

#利用私钥解密的函数解密数据
class AssetView(APIAuthView):
    def post(self, request, *args, **kwargs):
        body = decrypt(request._request.body)           #通过decrypt 解密数据,body 中才是原生的数据
        asset_info = json.loads(body.decode('utf-8'))   #解密后的数据解码再序列化加载
View Code

猜你喜欢

转载自www.cnblogs.com/fanggege/p/10425528.html