背景:由于此网站postman能够请求,但requests无法发起请求,会报SSL错误,打开抓包工具时即可发起requests请求 ,如果出现如上症状,即可能为本次案例情况!!(一定要看重点!!!)
版本:requests==2.22.0
1、问题重现
具体报错为:requests.exceptions.SSLError: HTTPSConnectionPool
案例链接为:需要加群
其中postman请求如下(如此正常):
但是一旦使用requests请求就会犯病,具体如下(SSLError):
其中大多数人一般都会想到以下一个问题,是不是开了抓包工具(代理等)?
然而网上大多数此问题搜索到的解决方案一般有两个。
- 关闭代理,然后再次发起请求:然后over
- requests请求时加入参数verify=False
这两个方法一般能够解决80%的问题。但本人试过了,不行!🙅🙅♂️还试过aiohttp、scrapy等,都会报SSL错误。
以下是打开抓包工具发起请求的情况(如此正常):
2、问题分析
2.1、总结:我把抓包工具关了就无法请求,开着就能够请求,并且postman能够请求,与一般问题背道而驰。??
那说明验证的并非请求头,如果一直开着抓包工具,然后请求,得出结果就完了,但是这样也不符合长期稳定采集数据的逻辑,如果以后遇到这种网站,而项目要部署在服务器上的,也不可能一边开着个抓包工具,一边请求,况且服务器用的都是linux,fiddler没有linux。
2.2、思考:开了fiddler跟没开fidder有什么区别,最容易想到的代理证书的区别,fiddler伪造https证书。
以下这个回答给了我灵感:🔗链接在这😄
翻译如下:
客户端发起https的请求第一步是向服务器发送tls握手请求,其中就包含了客户端的一些特征,相关内容在tls协议报文中Client Hello的Transport Layer Security当中
2.4、wireshark工具
Wireshark(前称Ethereal)是一个网络封包分析软件。网络封包分析软件的功能是截取网络封包,并尽可能显示出最为详细的网络封包资料。Wireshark使用WinPCAP作为接口,直接与网卡进行数据报文交换。
🔗下载链接
先看postman的正常请求接口的数据:
选中client Hello包,展开Transport Layer Security
这个JA3算法。这个算法在官网上面的介绍信息如下:
The JA3 algorithm takes a collection of settings from the SSL “Client Hello” such as SSL/TLS version, accepted cipher suites, list of extensions, accepted elliptic curves, and elliptic curve formats.
JA3!!!!
到这其实问题就很明朗了,JA3算法收集了SSL请求里面的信息,包括但不限于SSL/TLS版本,Cipher Suites数量,浏览器扩展列表,elliptic curves等等。通过这一系列参数综合起来生成一个指纹字符串。
3、问题解决
通过问题的分析与重现并利用wireshark工具对比client Hello包,发现requests中的密码套件Cipher Suites与postman请求的数量差了一倍以上,最后生成的Fullstring指纹字符串肯定对不上。
3.1、Cipher Suites差异解决
事实上在requests里面,要修改Cipher Suits中的加密算法,需要修改urllib3里面的ssl上下文,并实现一个新的HTTP适配器(HTTPAdapter)。
debug跟踪到了几处可能可以修改TLS握手特征的代码
路径如下:/usr/local/lib/python3.9/site-packages/urllib3/util/ssl_.py
这些就是requests的加密算法了
通过对比加密套件,选取了与成功请求具有交集的加密方式,具体代码如下
# encoding: utf-8
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.ssl_ import create_urllib3_context
ciphers = ":".join(
[
"DH+AES",
"RSA+AES",
]
)
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.93 Safari/537.36',
}
class DESAdapter(HTTPAdapter):
"""
更改适配器,修改加密算法
"""
def init_poolmanager(self, *args, **kwargs):
context = create_urllib3_context(ciphers=ciphers)
kwargs['ssl_context'] = context
return super(DESAdapter, self).init_poolmanager(*args, **kwargs)
在一般情况下,当我们实现一个子类的时候,__init__的第一行应该是super().init(*args, **kwargs),但是由于init_poolmanager是复写了父类的方法,这个方法是在执行super().init(*args, **kwargs)的时候就执行的。所以,我们随机设置 Cipher Suits 的时候,需要放在super().init(*args, **kwargs)的前面。
有了适配器以后,我们使用requests时,初始化一个Session,然后把这个适配器绑定到特定的网站上:
def get_page(session):
response = session.get(f'https://*test*/6891?pageIndex=3', headers=headers, verify=False)
print(response.text)
session = requests.Session()
session.mount('https://*test*.cn', DESAdapter())
get_page(session)
其中,session.mount的第一个参数表示这个适配器只在https://test.cn开头的网址中生效。
原本内容:
Cipher Suites Length: 86
Cipher Suites (43 suites)
修改后的内容:
Cipher Suites Length: 60
Cipher Suites (30 suites)
3.2、成功请求
3.3、结论:
加密套件的内容发生了变化,使得Finger Print和原本requests不一致,理论上其他客户端也可以进行修改代码实现变更TLS指纹的操作,但是如java,go等编译型语言写的工具在没有源码的情况下修改会很麻烦。
人生苦短,我学Python!!!😊 :】