Python Requests中TLS握手配置调整及403问题解决问询
Python Requests TLS握手填充导致403:如何取消握手填充?
用Python Requests爬取目标站点已有一年,近一周每次请求均返回HTTP 403错误,但浏览器和curl工具可正常访问。通过WireShark抓包发现核心差异:Python/OpenSSL的TLS握手会添加填充,而浏览器和curl则不会。现需调整TLS握手行为取消填充,问题如下:
- 是否可通过Requests的配置选项直接实现该修改?
- 若不行,如何通过修改SSL库调用或源码实现?
返回403的Python Requests代码
import requests s = requests.session() s.headers = { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 'Accept-Language': 'pt-BR,pt;q=0.9', 'Cache-Control': 'max-age=0', 'Connection': 'keep-alive', 'DNT': '1', 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-Site': 'none', 'Sec-Fetch-User': '?1', 'Upgrade-Insecure-Requests': '1', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', 'sec-ch-ua': '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"Windows"', } response = s.get( 'https://www63.bb.com.br/portalbb/djo/id/resgate/dadosResgate.bbx' )
可正常访问的curl命令
curl "https://www63.bb.com.br/portalbb/djo/id/resgate/dadosResgate.bbx" ^ -H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" ^ -H "Accept-Language: pt-BR,pt;q=0.9" ^ -H "Cache-Control: max-age=0" ^ -H "Connection: keep-alive" ^ -H "DNT: 1" ^ -H "Sec-Fetch-Dest: document" ^ -H "Sec-Fetch-Mode: navigate" ^ -H "Sec-Fetch-Site: none" ^ -H "Sec-Fetch-User: ?1" ^ -H "Upgrade-Insecure-Requests: 1" ^ -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" ^ -H "sec-ch-ua: \"Chromium\";v=\"130\", \"Google Chrome\";v=\"130\", \"Not?A_Brand\";v=\"99\"" ^ -H "sec-ch-ua-mobile: ?0" ^ -H "sec-ch-ua-platform: \"Windows\""
解决方案
1. Requests原生配置无法直接控制TLS握手填充
Requests本身没有暴露控制TLS握手填充的配置选项,因为这属于底层OpenSSL的行为,Requests的SSL配置基于urllib3和Python标准库ssl模块,而ssl模块默认未提供该级别的控制接口。
2. 可行的实现方式
方法一:使用curl_cffi模拟curl的TLS行为(最简单可靠)
curl_cffi直接调用curl的底层实现,TLS握手逻辑与curl完全一致,可彻底避免填充差异导致的403。
首先安装依赖:
pip install curl_cffi
修改后的代码:
from curl_cffi import requests s = requests.Session() s.headers = { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 'Accept-Language': 'pt-BR,pt;q=0.9', 'Cache-Control': 'max-age=0', 'Connection': 'keep-alive', 'DNT': '1', 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-Site': 'none', 'Sec-Fetch-User': '?1', 'Upgrade-Insecure-Requests': '1', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', 'sec-ch-ua': '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"Windows"', } # 模拟Chrome 130的TLS行为,与curl匹配 response = s.get( 'https://www63.bb.com.br/portalbb/djo/id/resgate/dadosResgate.bbx', impersonate="chrome130" )
方法二:自定义SSL上下文(原生Requests适配)
若坚持使用原生Requests,可通过自定义SSL上下文,禁用部分可能导致指纹差异的选项,并匹配浏览器的TLS参数,虽无法完全取消填充,但可缩小差异:
import requests from requests.adapters import HTTPAdapter from urllib3.poolmanager import PoolManager import ssl class TLSAdapter(HTTPAdapter): def init_poolmanager(self, *args, **kwargs): # 创建自定义SSL上下文 ctx = ssl.create_default_context() # 匹配浏览器的TLS版本范围 ctx.min_version = ssl.TLSVersion.TLSv1_2 ctx.max_version = ssl.TLSVersion.TLSv1_3 # 禁用压缩和会话票证,减少指纹差异 ctx.options |= ssl.OP_NO_COMPRESSION ctx.options |= ssl.OP_NO_TICKET # 使用浏览器常用的椭圆曲线 ctx.set_ecdh_curve('secp256r1') # 禁用不必要的扩展 ctx.options |= ssl.OP_NO_RENEGOTIATION kwargs['ssl_context'] = ctx return super().init_poolmanager(*args, **kwargs) # 挂载适配器到会话 s = requests.Session() s.mount('https://', TLSAdapter()) s.headers = { # 保持原headers不变 } response = s.get('https://www63.bb.com.br/portalbb/djo/id/resgate/dadosResgate.bbx')
方法三:底层修改OpenSSL行为(复杂)
若需彻底取消TLS握手填充,需通过pyopenssl或cryptography库直接操作OpenSSL的Client Hello回调,手动构建无填充的Client Hello。该方法实现难度大,需对TLS协议和OpenSSL API有深入了解,示例如下(基于pyopenssl):
首先安装依赖:
pip install pyopenssl
import requests from requests.adapters import HTTPAdapter from urllib3.contrib.pyopenssl import PyOpenSSLContext from urllib3.poolmanager import PoolManager from OpenSSL import SSL def client_hello_callback(conn, client_hello): # 手动修改Client Hello,移除填充字段 # 需解析并重构Client Hello数据包,此处仅为示例框架 modified_hello = client_hello[:-4] # 假设最后4字节是填充,实际需根据协议解析 return modified_hello class NoPaddingTLSAdapter(HTTPAdapter): def init_poolmanager(self, *args, **kwargs): ctx = PyOpenSSLContext(SSL.TLSv1_2_METHOD) # 设置Client Hello回调 ctx.set_client_hello_callback(client_hello_callback) kwargs['ssl_context'] = ctx return super().init_poolmanager(*args, **kwargs) # 使用适配器 s = requests.Session() s.mount('https://', NoPaddingTLSAdapter()) # 设置headers并请求
注意:该方法需要精准解析TLS Client Hello的结构,极易出错,仅适合高级场景。
内容的提问来源于stack exchange,提问作者Mauro Moreira
相关产品推荐
相关产品推荐

