同一个Python进程,普通get能访问私有CA签发的HTTPS接口,改成先准备请求再send,就报CERTIFICATE_VERIFY_FAILED。环境变量明明没变,证书也没换。这时先别重装根证书:可能是换了调用入口,却没把环境CA设置带过去。
本文只讨论Requests默认传输适配器的这条路径,不覆盖自定义Adapter、HTTPS代理双层证书或操作系统信任库安装。目标是保留证书验证,找出参数在哪一步丢失。
一、先把请求对象与连接设置分开
PreparedRequest负责准备要发送的请求,例如URL、头部和请求体;它不是一张“已经通过TLS验收”的通行证。Session.prepare_request还能应用会话级状态,例如Cookie,但这并不等于已经合并REQUESTS_CA_BUNDLE。
Requests官方Advanced Usage明确提醒:prepared request流程不会自动按普通请求入口那样采用环境设置,环境指定的自签信任材料可能因此未被使用。这里的重点是CA合并路径,不能扩大成“send完全不看任何环境”;受检版本的send仍可能解析环境代理。

图为四项职责示意,不是完整调用状态机。环境合并还涉及代理等参数;最后仍须独立验证证书、HTTP状态和业务正文,不能把得到settings当作请求成功。
二、先确认运行进程真的拿到了CA
检查实际解释器、Requests版本、进程环境中的REQUESTS_CA_BUNDLE,以及文件是否存在、当前用户是否可读、内容是否为授权信任的CA证书。只在终端里export,不证明服务进程或容器已继承;不要打印整份环境,里面可能有凭据。
如果该变量未设置,官方文档说明会回退检查CURL_CA_BUNDLE。CA路径不是客户端私钥路径;cert参数用于客户端证书,verify用于服务端证书验证,别把两者交换。也不要只比较文件名:指向错误CA的可读文件依然不可信。
三、需要环境配置时,显式合并后再发送
下面是可复制的调用模板。前提是安装Requests,已有可访问的HTTPS接口和可信CA文件,并由启动环境提供REQUESTS_CA_BUNDLE。将示例URL替换为自己的授权目标;example.org不是本文搭建的服务。
from requests import Request, Session
url = "https://api.example.org/health"
with Session() as s:
prepped = s.prepare_request(Request("GET", url))
settings = s.merge_environment_settings(
prepped.url, {}, None, None, None
)
with s.send(prepped, timeout=(3, 10), **settings) as r:
r.raise_for_status()
print(r.status_code, r.text)
这里合并的是CA路径配置,不是拼接证书文件;默认trust_env=True,且verify为True或None时才采用环境CA。使用Session.prepare_request保留会话状态,再将字典传给同一Session.send。合并函数参数依次为URL、代理、流式读取、verify、客户端证书,None不是关闭校验。
空代理字典也不代表“禁用环境代理”。另外,settings已经含verify等键,不要再把同名关键字重复传给send。timeout元组分别限制连接和读取等待,不是整个业务的总截止时间;print也只是观测,真实业务还需检查约定字段。
四、不想依赖环境,就显式指定信任文件
对于边界明确的内部调用,可采用另一种配置方式。以下模板刻意关闭环境继承,并显式传入CA路径;它与上一段是替代方案,不必拼在一起。生产若依赖代理或默认认证,需要另行配置,不能直接照抄trust_env=False。
from requests import Request, Session
url = "https://api.example.org/health"
ca_file = "/path/to/private-root.pem"
with Session() as s:
s.trust_env = False
prepped = s.prepare_request(Request("GET", url))
with s.send(prepped, verify=ca_file, timeout=(3, 10)) as r:
r.raise_for_status()
print(r.status_code, r.text)
本机受检实现中,trust_env=False会跳过环境CA读取;这不是专门的CA开关。也不要认为给s.verify赋值就永远压过环境:普通请求入口或显式合并时,环境CA仍可能参与优先级。最终检查传给send的verify,而不是只看Session属性。
五、用隔离HTTPS把正反例跑出来
本次实测环境为Python3.14.3、Requests2.34.2,Python链接的OpenSSL与命令行均为1.1.1k。实验在随机回环端口启动临时HTTPS服务,临时根签发带localhost名称的服务证书;另建无关根作负例。每组新建Session,清除代理等相关环境干扰,不修改系统信任库或生产配置。
本地夹具将上述模板的URL和CA占位路径替换为临时资源,并从最终HTML提取代码执行。普通get加正确环境CA成功;准备后直接send失败;显式合并成功;trust_env=False且只依赖环境失败;显式正确CA成功;显式无关CA失败。错误名称另测失败,说明合并环境不会绕过名称验证。
成功组不仅返回200,还核对正文ok和服务端实际HTTP事件;失败组确认没有进入HTTP处理。此结果证明指定版本、默认适配器和隔离服务的行为,不是用户真实接口、代理或生产部署验收。随文交付的是调用模板,临时签发服务为内部测试夹具,不要求读者运行未提供的脚本。
六、按现象定位,不要一律换CA
| 现象 | 优先核对 | 不能据此推导 |
|---|---|---|
| get成功,直接send失败 | 是否遗漏环境合并、verify最终值 | 服务端证书突然损坏 |
| 合并后仍失败 | trust_env、错误CA、名称、证书链 | 调用合并函数就必然可信 |
| CA路径不可读 | 运行用户、绝对路径和文件内容 | 关闭校验即可正式上线 |
| 200但业务不对 | 目标、响应字段与业务契约 | TLS通过就代表操作完成 |
七、上线前保留这份验收清单
核对真实进程版本与配置来源;确认可信CA的授权范围;区分请求准备和环境合并;检查实际verify及代理配置;用新Session测正确CA、错误CA和错误名称;最后验收HTTP状态及业务内容。记录失败原文时脱敏URL参数、认证头和代理凭据。
不要用verify=False或屏蔽InsecureRequestWarning当修复。前者放弃关键身份检查,后者只是让提醒消失。证书问题没解决,日志先安静了,这种“成功”不值得庆祝。
本文聚焦Python Requests库中PreparedRequest调用路径下SSL证书验证失败问题,指出prepare_request不自动合并REQUESTS_CA_BUNDLE等环境CA配置,导致CERTIFICATE_VERIFY_FAILED。重点分析环境变量继承、verify参数传递、trust_env行为、显式CA合并机制,并提供可复现的隔离测试方案与生产验收清单,强调保留证书验证前提下的精准定位方法。
372

被折叠的 条评论
为什么被折叠?



