Python向带有SSL/TSL认证服务器发送网络请求小实践(附并发http请求实现asyncio+aiohttp)

1. 写在前面

最近工作中遇到这样的一个场景:给客户发送文件的时候,为保证整个过程中,文件不会被篡改,需要在发送文件之间, 对发送的文件进行签名, 而整个签名系统是另外一个团队做的, 提供了一个接口服务完成签名,但访问这个接口需要提供他们团队提供的证书链先进行认证,所以需要和该服务端建立安全的链路,这里是用ssl双向认证的方式实现。

本篇文章主要是记录下如果是用Python给这种ssl双向认证的服务器发送post请求的时候,应该怎样携带证书去双向认证? 整个过程怎么实现的,如果需要处理的文件太多, 请求响应超时的情况下, 又怎么把同步http请求改成并发的http请求以减少响应时间?

ok, let’s go!

2. 基础知识

2.1 SSL/TSL

SSL是安全套接层(secure sockets layer),而TLS是SSL的继任者,叫传输层安全(transport layer security),是为网络通信提供安全及数据完整性的一种安全协议。TLS与SSL在传输层对网络连接进行加密。比如HTTP协议是明文传输,加上SSL层之后,就有了雅称HTTPS。

SSL/TLS双向认证,也被叫做客户端证书认证,是一个验证客户端和服务器双方身份的过程,用来确保双向的通信安全性。
在单向SSL/TLS认证中,只有服务器需要向客户端提供证书用以证明自己的身份。而在双向认证中,客户端也需要使用证书来证明自己的身份,服务器才会允许连接和交换信息。

这种认证流程增加了安全性,确保通信双方都可信。

整个握手认证流程大概是这样:
在这里插入图片描述
双向认证的流程通常如下:

  1. 握手开始:客户端向服务器发送一个连接请求,告知服务器它支持的SSL/TLS版本和加密算法等。
  2. 服务器证书:服务器响应客户端的请求,并发送它的SSL/TLS证书给客户端。这个证书中包含了服务器的公钥和一些身份信息,并且由可信的证书颁发机构(CA)签发。
  3. 证书验证:客户端验证服务器证书的合法性,它会检查证书是否过期、是否被CA真实签发、域名是否匹配等。
  4. 客户端密钥交换:客户端根据服务器提供的公钥加密一个随机生成的对称加密密钥,并发送给服务器。
  5. 客户端证书请求:服务器向客户端发送一条请求消息,要求客户端也提供一份证书。
  6. 客户端证书:客户端发送它的SSL/TLS证书给服务器。如同服务器证书一样,客户端证书也包含了客户端的公钥和身份信息,并且由CA签发。
  7. 服务器验证客户端证书:服务端将验证客户端提供的证书,检查是否由信任的CA签发、是否过期、是否被吊销等。
  8. 预主密钥生成:客户端根据之前的握手过程生成一个预主密钥(Pre-Master Secret)。
  9. 安全参数生成:客户端和服务器都使用预主密钥和一些已经交换的信息计算出会话密钥。
  10. 完成握手:双方利用生成的会话密钥进行加密通讯。

在整个过程中,SSL/TLS的加密算法保证了传输内容的隐私和完整性。双向认证增加了一层安全性,因为不仅要验证服务器的身份,客户端的身份也同样需要验证。这在需要高安全性的场合,比如金融交易、企业系统访问中非常有用。

当然这个过程简单了解下即可,主要是能在实际场景中会使用。

2.2 证书

CA: 证书授权中心( certificate authority)。类似于国家出入境管理处一样,给别人颁发护照;也类似于国家工商管理局一样,给公司企业颁发营业执照。 它有两大主要性质:

  1. CA本身是受信任的 // 国际认可的
  2. 给他受信任的申请对象颁发证书

生成和管理SSL/TLS证书通常涉及以下几个步骤:

  • 创建密钥对(公钥和私钥)
    • 使用密码学工具(例如OpenSSL)创建一对密钥。
    • 私钥必须安全保管,确保只有授权个体才能访问。
  • 创建证书签名请求(CSR):
    • 使用私钥生成CSR。
    • CSR中包括公钥和关于个人或组织身份的信息(如组织名称、常用名称(CN)、组织单位、城市、国家等)。
  • 提交CSR到证书颁发机构(CA):
    • 将CSR提交给CA。
    • CA验证请求者提供的信息并决定是否发放证书。
  • 获取证书:
    • CA签发证书后,它通常会提供一个X.509格式的证书文件(也就是SSL/TLS证书)。
  • 安装证书:
    • 将证书安装在服务器或者需要用到的设备上。
    • 一些服务可能还需要你安装中间证书,以建立证书链至根CA。
  • 配置使用SSL/TLS的服务:
    • 配置服务器使用安装的证书和私钥。
    • 调整服务器设置以启用SSL/TLS加密。
  • 证书的相关维护等

那假设有了这个证书,实际过程中应该怎么用呢? 从服务器的角度:

  1. SSL Server生成一个公钥/私钥对(server.key/server.pub), 私钥加密, 公钥解密。
  2. server.pub 生成一个请求文件 server.req. 请求文件中包含有 server 的一些信息,如域名/申请者/公钥等
  3. server 将请求文件 server.req 递交给 CA,CA验明正身后,将用 ca.key和请求文件加密生成 server.crt
  4. 由于 ca.key 和 ca.crt 是一对, 于是 ca.crt 可以解密 server.crt.


在实际应用中:如果 SSL Client 想要校验 SSL server.那么 SSL server 必须要将他的证书 server.crt 传给 client.然后 client 用 ca.crt 去校验 server.crt 的合法性。如果是一个钓鱼网站,那么CA是不会给他颁发合法server.crt证书的,这样client 用ca.crt去校验,就会失败。比如浏览器作为一个 client,你想访问合法的淘宝网站https://www.taobao.com, 结果不慎访问到 https://wwww.jiataobao.com ,那么浏览器将会检验到这个假淘宝钓鱼网站的非法性,提醒用户不要继续访问!这样就可以保证了client的所有https访问都是安全的

好了,上面的这些内容主要是先对SSL/TSL以及证书等有个大致印象, 下面看看在实际业务中是怎么使用的。

3. 业务落地实践

场景: 我有一个目录需要打成压缩包发给客户, 但是打包之前,为了防止网络传输过程中被篡改, 需要对里面的每个文件进行签名, 这样客户收到文件后再验签就知道是否被改动了。

签名是调用一个签名服务的接口完成,但这个签名服务是采用TSL认证的,如果想给它发请求去签名,需要申请client的证书,携带着证书去发送post请求(把文件内容发给服务器,服务器返回一个签名好的字符串)

这个过程用流程图表示如下:
在这里插入图片描述
流程就是:

  1. 客户端这边, 先生成一个CSR,可以理解成一个公钥一个私钥, 可以用openssl来实现

    # 生成私钥
    openssl ecparam -out private.key -name prime256v1 -genkey# 生成P8格式的pem私钥
    openssl pkcs8 -topk8 -inform PEM -in  private.key -outform pem -nocrypt -out  private.key.pem# 生成CSR文件
    openssl  req -new -out pki-tls-ADD.csr -key  private.key.pem -subj /CN=pki-tls-ADD
    
  2. 拿着这个pki-client-ADD.csr文件, 去申请证书

  3. 证书完成后, 会发回来一个server_chain.pem的证书链, 一个pki-tls-ADD.crt.pem的证书文件(PKI根据CSR颁发的客户端pem证书文件)

  4. 发送请求的时候,要在上下文里面带着3个文件去认证:

    cert_file_path = "/home/wuzhongqiang/csr_file/pki-tls-ADD.crt.pem"
    key_file_path = "/home/wuzhongqiang/csr_file/private.key.pem"
    ca_file_path = "/home/wuzhongqiang/csr_file/server_chain.pem"
    

下面是一个同步请求的代码:

import base64
import hashlib
import json
import os
import random
import ssl
import time
import urllib
import aiohttp
import asyncio
from urllib.parse import quoteimport requests
from requests.adapters import HTTPAdapter
from urllib3 import PoolManagerclass SSLAdapter(HTTPAdapter):def __init__(self, certificate, private_key, ca_file, *args, **kwargs):self._certificate = certificateself._private_key = private_keyself._ca_file = ca_filesuper().__init__(*args, **kwargs)def init_poolmanager(self, *args, **kwargs):context = ssl.create_default_context()context.load_cert_chain(certfile=self._certificate, keyfile=self._private_key)context.load_verify_locations(cafile=self._ca_file)self.poolmanager = PoolManager(*args, ssl_context=context, **kwargs)class SignClient(object):IP = "https://wuzhongqiang.net"SIGN_URL = "/api/v1/sign_file"APP_KEY = os.environ.get("APP_KEY", "xxx")APP_SECRET = os.environ.get("APP_SECRET", "xxx")@staticmethoddef gen_app_key_sign(method: str, url: str, header: dict, security_key: str, body: str):http_str = (f"{method}{url}"f"es-auth-appkey:{header['es-auth-appkey']}"f"es-auth-nonce:{header['es-auth-nonce']}"f"es-auth-timestamp:{header['es-auth-timestamp']}"f"json={body}"f"{security_key}")try:urlCodeStr = urllib.parse.quote(http_str, safe='')hashHex = hashlib.sha256(urlCodeStr.encode('utf-8')).hexdigest()except Exception as e:print(e)hashHex = ""return hashHex@staticmethoddef sign_file(data: str, cert_file_path: str, key_file_path: str, ca_file_path: str) -> str:target_data = {"data": data}es_auth_noce = ''.join(random.choices('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', k=16))es_auth_timestamp = str(int(time.time() * 1000))auth_info = {"es-auth-appkey": SignClient.APP_KEY,"es-auth-nonce": es_auth_noce,"es-auth-timestamp": es_auth_timestamp}payload = json.dumps(target_data, separators=(',', ':'))signature_value = SignClient.gen_app_key_sign(method="POST",url=SignClient.SIGN_URL,header=auth_info,security_key=SignClient.APP_SECRET,body=payload)# print(f"target_data: {target_data}\n"#       f"es-auth-appkey:{SignClient.APP_KEY}\n"#       f"es-auth-nonce:{es_auth_noce}\n"#       f"es-auth-timestamp:{es_auth_timestamp}\n"#       f"es-auth-sign: {signature_value}")headers = {"Content-Type": "application/json","es-auth-appkey": SignClient.APP_KEY,"es-auth-nonce": es_auth_noce,"es-auth-timestamp": es_auth_timestamp,"es-auth-sign": signature_value}session = requests.Session()session.mount('https://', SSLAdapter(certificate=cert_file_path,private_key=key_file_path,ca_file=ca_file_path))response = session.post(url=f"{SignClient.IP}{SignClient.SIGN_URL}", data=payload, headers=headers)# requests method# response = requests.request("POST",#                             url=f"{SignClient.IP}{SignClient.SIGN_URL}",#                             headers=headers,#                             data=payload,#                             cert=(cert_file_path, key_file_path),#                             verify=ca_file_path)try:response_body = response.textjson_object = json.loads(response_body)sign_data = json_object["signData"]return sign_dataexcept Exception as e:print(e)return ""if __name__ == '__main__':base_dir = "/home/wuzhongqiang/Downloads/test_dir"cert_file_path = "/home/wuzhongqiang/csr_file/pki-tls-ADD.crt.pem"key_file_path = "/home/wuzhongqiang/csr_file/private.key.pem"ca_file_path = "/home/wuzhongqiang/csr_file/server_chain.pem"start = time.time()for root, _, files in os.walk(base_dir):for file in files:file_path = os.path.join(root, file)with open(file_path, 'rb') as f:base64_data = base64.b64encode(f.read())base64_data_str = base64_data.decode()sign_data = SignClient.sign_file(data=base64_data_str,cert_file_path=cert_file_path,key_file_path=key_file_path,ca_file_path=ca_file_path)dir_name = os.path.dirname(file_path)file_name = os.path.basename(file_path)sign_file_name = f"{dir_name}/{file_name}.sign"print(f"target file: {file_path}, sign file: {sign_file_name}")with open(sign_file_name, 'wb') as f:f.write(sign_data.encode())print(f"use time: {time.time() - start} s")   # 46s

requests库是同步阻塞的,必须等到结果才会发第二个请求。 这个代码如果部署成服务的方式,是会发生接口访问请求超时,所以下面通过asyncio+aiohttp实现并发http请求, 来减少请求响应时间, 下面是一个异步并发http请求的代码实现。

class SignClient(object):IP = "https://wuzhongqiang.net"SIGN_URL = "/api/v1/sign_file"APP_KEY = os.environ.get("APP_KEY", "xxx")APP_SECRET = os.environ.get("APP_SECRET", "xxx")@staticmethoddef gen_app_key_sign(method: str, url: str, header: dict, security_key: str, body: str):http_str = (f"{method}{url}"f"es-auth-appkey:{header['es-auth-appkey']}"f"es-auth-nonce:{header['es-auth-nonce']}"f"es-auth-timestamp:{header['es-auth-timestamp']}"f"json={body}"f"{security_key}")try:urlCodeStr = urllib.parse.quote(http_str, safe='')hashHex = hashlib.sha256(urlCodeStr.encode('utf-8')).hexdigest()except Exception as e:print(e)hashHex = ""return hashHex@staticmethodasync def sign_file(session: aiohttp.ClientSession, data: str, url: str, file_path: str, ssl_context) -> typing.Tuple[str, str]:target_data = {"data": data}es_auth_noce = ''.join(random.choices('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', k=16))es_auth_timestamp = str(int(time.time() * 1000))auth_info = {"es-auth-appkey": SignClient.APP_KEY,"es-auth-nonce": es_auth_noce,"es-auth-timestamp": es_auth_timestamp}payload = json.dumps(target_data, separators=(',', ':'))signature_value = SignClient.gen_app_key_sign(method="POST",url=SignClient.SIGN_URL,header=auth_info,security_key=SignClient.APP_SECRET,body=payload)headers = {"Content-Type": "application/json","es-auth-appkey": SignClient.APP_KEY,"es-auth-nonce": es_auth_noce,"es-auth-timestamp": es_auth_timestamp,"es-auth-sign": signature_value}dir_name = os.path.dirname(file_path)file_name = os.path.basename(file_path)sign_file_name = f"{dir_name}/{file_name}.sign"async with session.post(url=f"{SignClient.IP}{SignClient.SIGN_URL}", data=payload, headers=headers, ssl_context=ssl_context) as response:try:response_body = await response.text()json_object = json.loads(response_body)sign_data = json_object["signData"]return sign_file_name, sign_dataexcept Exception as e:print(e)return "", ""@staticmethodasync def sign_files(base_dir: str,cert_file_path: str,key_file_path: str,ca_file_path):ssl_context = ssl.create_default_context()# 加载私钥及对应证书ssl_context.load_cert_chain(certfile=cert_file_path, keyfile=key_file_path)# 加载一组用于验证其他对等方证书的 "证书颁发机构" (CA) 证书ssl_context.load_verify_locations(cafile=ca_file_path)async with aiohttp.ClientSession() as session:tasks = []for root, _, files in os.walk(base_dir):for file in files:file_path = os.path.join(root, file)with open(file_path, 'rb') as f:base64_data = base64.b64encode(f.read())base64_data_str = base64_data.decode()# 创建一个签名的任务并添加到任务列表task = SignClient.sign_file(session=session,data=base64_data_str,url=f"{SignClient.IP}{SignClient.SIGN_URL}",file_path=file_path,ssl_context=ssl_context)tasks.append(task)results = await asyncio.gather(*tasks)return resultsif __name__ == '__main__':base_dir = "/home/wuzhongqiang/Downloads/test_dir"cert_file_path = "/home/wuzhongqiang/csr_file/pki-tls-ADD.crt.pem"key_file_path = "/home/wuzhongqiang/csr_file/private.key.pem"ca_file_path = "/home/wuzhongqiang/csr_file/server_chain.pem"start = time.time()# 如果已经开启了event, 也就是在服务里面调用的话,这里直接await 后面的签名函数即可results = asyncio.run(SignClient.sign_files(base_dir, cert_file_path, key_file_path, ca_file_path))for sign_file_name, sign_data in results:if sign_data:print(f"sign file: {sign_file_name}")with open(sign_file_name, 'wb') as f:f.write(sign_data.encode())else:print(f"Signed filed written to: {sign_file_name}")print(f"use time: {time.time() - start} s")   # 0.58s

改成并发http请求之后, 处理速度从之前的46s降到了0.58s, 效果还是非常可观的。

参考:

  • python关于SSL/TLS认证的实现
  • SSL接口文档

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/299501.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AI大语言模型GPT —— R 生态环境领域数据统计分析

自2022年GPT(Generative Pre-trained Transformer)大语言模型的发布以来,它以其卓越的自然语言处理能力和广泛的应用潜力,在学术界和工业界掀起了一场革命。在短短一年多的时间里,GPT已经在多个领域展现出其独特的价值…

数据挖掘入门项目二手交易车价格预测之建模调参

文章目录 目标步骤1. 调整数据类型,减少数据在内存中占用的空间2. 使用线性回归来简单建模3. 五折交叉验证4. 模拟真实业务情况5. 绘制学习率曲线与验证曲线6. 嵌入式特征选择6. 非线性模型7. 模型调参(1) 贪心调参(2)…

C++从入门到精通——初步认识面向对象及类的引入

初步认识面向对象及类的引入 前言一、面向过程和面向对象初步认识C语言C 二、类的引入C的类名代表什么示例 C与C语言的struct的比较成员函数访问权限继承默认构造函数默认成员初始化结构体大小 总结 前言 面向过程注重任务的流程和控制,适合简单任务和流程固定的场…

电商技术揭秘八:搜索引擎中的SEO内部链接建设与外部推广策略

文章目录 引言一、 内部链接结构优化1.1 清晰的导航链接1. 简洁明了的菜单项2. 逻辑性的布局3. 避免深层次的目录结构4. 使用文本链接5. 突出当前位置6. 移动设备兼容性 1.2 面包屑导航1. 显示当前页面位置2. 可点击的链接3. 简洁性4. 适当的分隔符5. 响应式设计6. 避免重复主页…

c# wpf XmlDataProvider 简单试验

1.概要 2.代码 <Window x:Class"WpfApp2.Window12"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d"http://schemas.microsoft.com/expression/blend…

Debian12 使用 nginx 与 php8.2 使用 Nextcloud

最近将小服务器升级了下系统&#xff0c;使用了 debian12 的版本&#xff0c;正好试试 nginx 和 php-fpm 这种方式运行 Nextcloud 这个私有云的配置。 一、基本系统及应用安装 系统&#xff1a;debian12 x86_64 位版本最小安装&#xff0c;安装后可根据自己需求安装一些工具&…

《图解Vue3.0》- 调试

如何对vue3项目进行调试 调试是开发过程中必备的一项技能&#xff0c;掌握了这项技能&#xff0c;可以很好的定义bug所在。一般在开发vue3项目时&#xff0c;有三种方式。 代码中添加debugger;使用浏览器调试&#xff1a;sourcemap需启用vs code 调试&#xff1a;先开启node服…

python标准数据类型--集合常用方法

在Python中&#xff0c;集合&#xff08;Set&#xff09;是一种无序且不重复的数据结构&#xff0c;它是由一个无序的、不重复的元素组成的。Python中的集合与数学中的集合概念相似&#xff0c;并且支持一系列常用的方法。本篇博客将深入介绍Python集合的常用方法&#xff0c;帮…

《QT实用小工具·十五》多种样式的开关控件

1、概述 源码放在文章末尾 目前实现了三种样式的开关控件按钮&#xff0c;如下所示&#xff1a; 项目部分代码如下所示&#xff1a; #ifndef IMAGESWITCH_H #define IMAGESWITCH_H/*** 图片开关控件 * 1. 自带三种开关按钮样式。* 2. 可自定义开关图片。*/#include <QWid…

小米汽车su7全色系展示源码

源码简介 小米汽车全色系展示源码&#xff0c;小米汽车su7全色系展示源码 安装教程 纯HTML&#xff0c;直接将压缩包上传网站目录解压即可 首页截图 源码下载 小米汽车su7全色系展示源码-小8源码屋源码简介 小米汽车全色系展示源码&#xff0c;小米汽车su7全色系展示源码 …

(二)小案例银行家应用程序-创建DOM元素

● 上图的数据很明显是从我们账户数组中拿到了&#xff0c;我们刚刚学习了forEach&#xff0c;所以我们使用forEach来创建我们的DOM元素&#xff1b; const displayMovements function (movements) {movements.forEach((mov, i) > {const type mov > 0 ? deposit : w…

如何在 Ubuntu 上安装和配置 Tomcat 服务器?

简介&#xff1a;最近有粉丝朋友在问如何在 Ubuntu 上安装和配置 Tomcat 服务器&#xff1f;今天特地写这篇文章进行解答&#xff0c;希望能够帮助到大家。 文章目录 Ubuntu上安装和配置Tomcat的详细步骤Tomcat在Linux环境下的安装与配置一、下载并上传Tomcat压缩包二、启动To…

Flutter开发进阶之错误信息

Flutter开发进阶之错误信息 在Flutter开发中错误信息通常是由Exception和Error表示&#xff0c;Error表示严重且不可恢复的错误&#xff0c;一般会导致程序直接终止&#xff0c;而Exception可以被显式抛出&#xff0c;一般为代码逻辑错误&#xff0c;根据Flutter的解释说Excep…

无监督学习简介

无监督学习简介 一、定义和核心概念 无监督学习的定义 无监督学习是机器学习的一个关键分支&#xff0c;它涉及到从未标注数据中学习和提取信息。不同于其他学习类型&#xff0c;无监督学习的数据集没有提供任何显式的输出标签或结果。因此&#xff0c;这种学习方法的主要任务…

最优乘车

题目描述 H 城是一个旅游胜地&#xff0c;每年都有成千上万的人前来观光。为方便游客&#xff0c;巴士公司在各个旅游景点及宾馆&#xff0c;饭店等地都设置了巴士站并开通了一些单程巴上线路。每条单程巴士线路从某个巴士站出发&#xff0c;依次途经若干个巴士站&#xff0c;…

力扣---分隔链表

给你一个链表的头节点 head 和一个特定值 x &#xff0c;请你对链表进行分隔&#xff0c;使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。 你应当 保留 两个分区中每个节点的初始相对位置。 示例 1&#xff1a; 输入&#xff1a;head [1,4,3,2,5,2], x 3 输出&a…

SSL VPN

1、SSL (Secure Sockets Layer)一种加密的通讯协定,用在使用者与网服器之间 【1】安全套接层 位于传输层和应用层之间,保护应用层的数据(HTTPS(443)=HTTP+TLS) 【2】版本 SSLv2 SSLv3 修改→TLS (Transport Layer Security)安全传输层协议,) 【3】模式 采用…

机器学习笔记 - 文字转语音技术路线简述以及相关工具不完全清单

一、TTS技术简述 今天的文本到语音转换技术(TTS)的目标已经不仅仅是让机器说话,而是让它们听起来像不同年龄和性别的人类。通常,TTS 系统合成器的质量是从不同方面进行评估的,包括合成语音的清晰度、自然度和偏好,以及人类感知因素,例如可理解性。 1、技术路线 (1)基…

linux 安装 pptp 协议

注意&#xff1a;目前iOS已不支持该协议 yum -y install ppp wget https://download-ib01.fedoraproject.org/pub/epel/7/x86_64/Packages/p/pptpd-1.4.0-2.el7.x86_64.rpm yum -y install pptpd-1.4.0-2.el7.x86_64.rpm vi /etc/pptpd.conf 去除 localip 和 remoteip的注释 …

基于沙漏 Tokenizer 的高效三维人体姿态估计框架HoT

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 摘要Abstract文献阅读&#xff1a;基于沙漏 Tokenizer 的高效三维人体姿态估计框架HoT1、研究背景2、提出方法3、模块详细3.1、什么是HoT3.2、HoT 框架3.3、Token 剪…