ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python SSL证书验证失败:从原理到解决方案的完整指南

Python SSL证书验证失败:从原理到解决方案的完整指南 1. 项目概述当Python请求遭遇SSL证书验证失败最近在写一个爬虫脚本用requests库去抓取一些公开的天气数据代码逻辑很简单就是requests.get(url)。本地调试一切正常但一把脚本放到内网那台刚装好Python3的CentOS服务器上跑立刻就给我甩了个脸子requests.exceptions.SSLError: HTTPSConnectionPool(host‘xxx’, port443): Max retries exceeded with url: /xxx (Caused by SSLError(SSLCertVerificationError(1, ‘[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1129)’)))。这个ssl: certificate_verify_failed错误对于经常用Python做网络请求的开发者来说绝对是个“老熟人”。它像一堵墙横在你和HTTPS服务器之间告诉你“我不信任你或者你不信任它”。尤其是在内网环境、使用自签名证书的服务、或者某些特定操作系统配置下这个问题几乎必然会出现。简单来说当Python的ssl模块requests库底层依赖它尝试与一个HTTPS服务器建立安全连接时它会验证服务器发来的数字证书是否可信。验证失败连接也就失败了。这个问题看似只是一个报错但其背后涉及HTTPS/TLS协议、公钥基础设施PKI、操作系统根证书库、Python环境管理等多个层面的知识。盲目地使用verifyFalse来绕过虽然能暂时让代码跑起来却引入了安全风险也失去了HTTPS加密传输的意义。本文将彻底拆解这个错误从根因分析到多种解决方案不仅告诉你如何“快速解决”更会让你明白“为什么要这样解决”以及在不同生产场景下如爬虫、内网服务调用、自动化脚本的最佳实践。2. 核心原理为什么Python会“不信任”一个证书要解决问题必须先理解问题。CERTIFICATE_VERIFY_FAILED这个错误的本质是证书链验证失败。我们来拆解一下这个过程。2.1 HTTPS握手与证书验证流程当你用requests.get(‘https://example.com’)时底层大致发生以下几步TCP连接首先与服务器的443端口建立TCP连接。TLS/SSL握手开始安全握手。客户端你的Python脚本发送“Client Hello”。服务器发送证书服务器回应“Server Hello”并附上自己的数字证书。这个证书包含了网站域名Common Name、颁发机构Issuer、有效期、以及一个非常重要的部分——服务器的公钥。客户端验证证书这是关键一步。你的Python环境具体是ssl模块需要验证这个证书是否可信。验证包括证书是否过期检查当前时间是否在证书的Valid from和Valid to之间。证书域名是否匹配检查证书中的域名或Subject Alternative Names是否与你请求的example.com匹配。证书链是否完整且可信这是最常出问题的一环。服务器证书通常不是由根证书颁发机构Root CA直接签发而是由中间CA签发。因此服务器需要提供完整的证书链从服务器证书到根证书。客户端需要用本地存储的受信任的根证书来逐级验证签名。2.2 证书链与“受信任的根证书库”想象一下护照验证。某个国家网站的护照服务器证书需要其上级机构中间CA的签章而该上级机构的合法性最终来源于一个国际公认的权威机构列表根证书库。你的电脑或Python环境里就存着这样一份“公认权威机构列表”即受信任的根证书库。在Windows/Mac上这个列表通常由操作系统提供和管理如Windows的证书存储Mac的Keychain。在Linux上通常由一个名为ca-certificates的软件包提供证书文件存放在/etc/ssl/certs/目录下并通过/etc/ssl/certs/ca-certificates.crt这个文件一个包含所有根证书的Bundle或/etc/pki/tls/certs/ca-bundle.crt来引用。Python如何找到它们Python的ssl模块在启动时会尝试定位这个根证书库。它有一个默认的搜索路径。如果找不到或者找到的库不完整就会导致无法验证任何由正规CA签发的证书。2.3 错误原因深度剖析unable to get local issuer certificate这个子错误信息非常明确地指出了问题客户端在本地找不到签发该服务器证书的CAIssuer。具体原因可能包括服务器配置不当服务器没有在TLS握手时发送完整的证书链缺少中间CA证书。这时客户端拿着服务器证书找不到签发它的中间CA自然无法追溯到受信任的根验证失败。客户端根证书库缺失或过时这是最常见的原因尤其在新装的操作系统、Docker基础镜像、或最小化安装的Linux服务器上。ca-certificates包没有安装或者安装的版本太旧缺少新的根证书。自签名证书或私有CA在内网环境中很多服务使用自己签发的证书自签名或由公司内部CA签发的证书。这些证书的根CA不在公开的受信任根证书库里因此默认不被信任。Python环境隔离在使用virtualenv或conda创建虚拟环境时如果创建时没有继承系统站点包或者虚拟环境内的certifi包Python世界常用的CA证书包有问题也可能导致验证失败。系统时间不正确如果客户端系统时间严重偏差如停留在过去或未来会导致证书有效期验证失败也可能触发此错误。注意很多教程一上来就教verifyFalse这相当于在护照检查时直接说“别查了我信他”。这虽然绕过了验证但也完全放弃了HTTPS的身份认证环节面临中间人攻击的风险。仅在测试、访问完全可控的内网自签名服务时临时使用并务必知晓其风险。3. 解决方案全景图从临时绕过到根治面对certificate_verify_failed我们有多种应对策略其安全性和适用场景各不相同。下图展示了从低级到高级的解决方案路径方案具体方法安全性适用场景本质临时绕过requests.get(verifyFalse)低(禁用验证)本地快速测试、抓取无关紧要的公开数据、访问已知可信的自签名服务放弃身份认证仅保留加密指向特定证书requests.get(verify‘/path/to/cert.pem’)中(自定义信任)访问使用特定自签名或私有CA证书的服务信任指定的单个或一批证书修复证书库更新系统ca-certificates包高(系统级修复)服务器证书由公共CA签发但客户端缺少根证书修复根本原因一劳永逸修复Python环境更新certifi包或设置REQUESTS_CA_BUNDLE高(应用级修复)Python虚拟环境证书库问题或需要指定非默认证书包确保Python使用的证书库是完整的脚本内嵌证书将证书内容写入代码或文件并用verify参数加载中高(精确控制)需要环境无关的部署或固定信任某几个特定CA将信任锚点内置于应用3.1 方案一临时禁用验证verifyFalse这是最“著名”也最不推荐的解决方案但在某些特定场景下能快速让程序跑起来。import requests # 为单个请求禁用SSL验证 response requests.get(‘https://example.com‘, verifyFalse) # 或者为整个requests会话禁用 session requests.Session() session.verify False response session.get(‘https://example.com‘)会发生什么执行上述代码时你会看到一个非常显眼的警告InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings InsecureRequestWarning这个警告是urllib3requests的底层库发出的它在大声提醒你正在做不安全的事情。如何关闭这个烦人的警告虽然不推荐但如果你确定要在测试环境这么做可以禁用这些警告import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)再次强调verifyFalse仅应用于你完全信任网络环境且不关心服务器真实身份的场合例如在隔离的测试环境中访问一个你自己搭建的、使用自签名证书的开发服务器。切勿在对公网服务或处理敏感数据的生产脚本中使用。3.2 方案二指定自定义证书文件或目录如果服务器使用的是自签名证书或私有CA签发的证书正确的做法是将该CA的根证书或服务器证书本身提供给requests让它去验证。步骤获取证书文件。如果是自签名证书向服务管理员索取.crt或.pem文件。如果是私有CA索取该CA的根证书。将证书文件放在项目目录或服务器特定路径下。在请求时通过verify参数指定该证书文件路径。import requests # 指定一个包含受信任CA的证书文件PEM格式 response requests.get(‘https://internal.company.com‘, verify‘./company-root-ca.crt‘) # 也可以指定一个包含多个PEM格式证书的目录注意目录需要已通过c_rehash处理较少用 # response requests.get(‘https://internal.company.com‘, verify‘./path/to/certs_dir/‘)实操心得PEM格式是什么证书文件通常是.crt,.cer,.pem后缀它们本质上都是文本文件内容以-----BEGIN CERTIFICATE-----开头以-----END CERTIFICATE-----结尾。你可以用文本编辑器打开查看。requests和底层的ssl模块需要的是这种PEM格式。常见问题我只有一个.p12或.pfx文件怎么办.p12或.pfx是包含私钥和证书的二进制格式通常用于客户端认证。requests的verify参数不能直接使用它。你需要从中提取出CA证书如果没有私钥的话。可以使用OpenSSL命令# 将.pfx转换为只包含证书的.pem会提示输入密码 openssl pkcs12 -in yourfile.pfx -out temp.pem -nokeys -nodes # 然后从生成的temp.pem中复制出证书部分BEGIN/END CERTIFICATE之间的内容到新文件。3.3 方案三修复系统根证书库推荐的根本解决方案对于访问公共互联网上的正规网站如https://www.baidu.com出现的验证失败最可能的原因是系统根证书库缺失。解决方法就是安装或更新它。Linux (Debian/Ubuntu)# 更新软件包列表 sudo apt-get update # 安装ca-certificates包 sudo apt-get install -y ca-certificates # 更新证书通常安装后会自动更新 sudo update-ca-certificates --fresh安装后系统的根证书库文件通常会在/etc/ssl/certs/ca-certificates.crt。Linux (CentOS/RHEL/Fedora)# 安装ca-certificates包 sudo yum install -y ca-certificates # 或使用dnf新版本Fedora/RHEL8 sudo dnf install -y ca-certificates # 更新 sudo update-ca-trust证书库文件通常在/etc/pki/tls/certs/ca-bundle.crt。验证安装安装完成后可以运行一个简单的Python脚本来测试import ssl print(ssl.get_default_verify_paths())这会输出Python当前使用的证书路径。检查cafile或capath指向的文件是否存在且不为空。3.4 方案四修复Python环境的证书库certifiPython社区有一个名为certifi的包它提供了一个精心维护的、Mozilla根证书列表的副本。requests库默认会尝试使用certifi提供的证书包。检查并更新certifi# 在您的Python环境中 pip install --upgrade certifi找到certifi的证书文件路径import certifi print(certifi.where()) # 输出类似/home/user/.virtualenvs/myenv/lib/python3.9/site-packages/certifi/cacert.pem这个cacert.pem文件就是requests默认使用的根证书库如果系统库不可用或requests优先选择它。手动指定requests使用certifi的证书即使系统库有问题你也可以强制requests使用certifi的证书。import requests import certifi response requests.get(‘https://example.com‘, verifycertifi.where())设置环境变量你也可以通过设置环境变量REQUESTS_CA_BUNDLE来全局指定证书包路径这样所有使用requests的代码都会生效无需修改代码。# Linux/macOS export REQUESTS_CA_BUNDLE/path/to/your/certificate_bundle.pem # Windows (命令行) set REQUESTS_CA_BUNDLEC:\path\to\your\certificate_bundle.pem # Windows (PowerShell) $env:REQUESTS_CA_BUNDLE“C:\path\to\your\certificate_bundle.pem”将/path/to/your/certificate_bundle.pem替换为certifi.where()的输出路径或你自定义的证书文件路径。3.5 方案五在代码中嵌入证书适用于打包或固定环境对于一些需要分发或环境高度固定的脚本你可以考虑将受信任的根证书直接嵌入到代码中或者作为资源文件随项目分发。方法A将证书内容写入临时文件import requests import tempfile # 你的CA证书PEM内容 CA_CERT_PEM “”“ -----BEGIN CERTIFICATE----- 你的证书内容... -----END CERTIFICATE----- ”“” # 创建临时文件并写入证书 with tempfile.NamedTemporaryFile(mode‘w‘, suffix‘.pem‘, deleteFalse) as tmp: tmp.write(CA_CERT_PEM) tmp_cert_path tmp.name try: response requests.get(‘https://internal.service.com‘, verifytmp_cert_path) finally: # 请求完成后删除临时文件 import os os.unlink(tmp_cert_path)方法B使用自定义Session全局配置如果你在一个项目中需要频繁访问同一个私有CA下的服务可以创建一个配置好的Session对象。import requests from requests.adapters import HTTPAdapter from urllib3.poolmanager import PoolManager import ssl class CustomSSLAdapter(HTTPAdapter): “”“自定义SSL Adapter使用指定的CA证书”“” def __init__(self, ssl_cert_path, **kwargs): self._ssl_cert_path ssl_cert_path super().__init__(**kwargs) def init_poolmanager(self, *args, **kwargs): # 创建PoolManager时传入自定义的SSL上下文 context ssl.create_default_context(cafileself._ssl_cert_path) kwargs[‘ssl_context‘] context return super().init_poolmanager(*args, **kwargs) # 使用 session requests.Session() adapter CustomSSLAdapter(‘./company-ca.crt‘) session.mount(‘https://‘, adapter) # 之后所有使用这个session发起的HTTPS请求都会使用自定义的CA证书进行验证 response1 session.get(‘https://service1.company.com‘) response2 session.get(‘https://service2.company.com‘)4. 针对特定场景的实战指南4.1 场景一在Docker容器中运行Python爬虫Docker镜像特别是python:slim或python:alpine这类精简镜像为了减小体积默认可能不包含ca-certificates包。这会导致容器内无法验证任何HTTPS证书。解决方案在Dockerfile中安装证书包。# 使用官方Python镜像作为基础 FROM python:3.9-slim # 1. 安装ca-certificates包并清理缓存以减小镜像层大小 RUN apt-get update apt-get install -y --no-install-recommends \ ca-certificates \ rm -rf /var/lib/apt/lists/* # 2. 可选但推荐升级pip和certifi RUN pip install --no-cache-dir --upgrade pip certifi # 3. 复制项目文件并安装依赖 WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . # 4. 运行你的脚本 CMD [“python“, “your_script.py“]关键点--no-install-recommends避免安装非必要的推荐包保持镜像精简。rm -rf /var/lib/apt/lists/*删除apt缓存是Docker镜像构建的最佳实践能显著减小最终镜像大小。即使安装了系统证书也升级certifi因为某些Python库可能更倾向于使用certifi。4.2 场景二访问使用自签名证书的内网服务公司内部的服务如测试环境、管理后台、数据平台等经常使用自签名证书。最佳实践获取证书从运维或服务负责人那里获取该服务的自签名证书文件.crt或.pem。集中管理将证书文件放在项目的一个固定目录下例如./certs/。代码配置方案A推荐使用环境变量指定证书路径提高灵活性。import os import requests INTERNAL_CA_CERT os.getenv(‘INTERNAL_CA_CERT_PATH‘, ‘./certs/internal-ca.crt‘) session requests.Session() session.verify INTERNAL_CA_CERT response session.get(‘https://internal-api.company.local‘)方案B在配置文件中定义。验证证书信息拿到证书后可以用OpenSSL检查其内容确保信息正确。openssl x509 -in ./certs/internal-ca.crt -text -noout查看Subject持有者、Issuer颁发者、Validity有效期。4.3 场景三处理requests爬虫中的SSL错误对于网络爬虫除了SSL验证问题还可能遇到429 Too Many Requests等错误。需要综合处理。import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry import time import logging # 配置日志 logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) def create_robust_session(ca_cert_pathNone, retries3, backoff_factor0.5): “”“创建一个健壮的requests会话支持重试和自定义SSL验证”“” session requests.Session() # 1. 配置SSL验证 if ca_cert_path: session.verify ca_cert_path logger.info(f“使用自定义CA证书: {ca_cert_path}“) else: # 默认使用系统/环境配置可以在这里强制使用certifi # session.verify certifi.where() pass # 2. 配置重试策略应对429等错误 retry_strategy Retry( totalretries, backoff_factorbackoff_factor, # 重试等待时间{backoff_factor} * (2^{重试次数-1}) 秒 status_forcelist[429, 500, 502, 503, 504], # 对这些状态码进行重试 allowed_methods[“GET“, “POST“] # 只对GET和POST方法重试 ) adapter HTTPAdapter(max_retriesretry_strategy) session.mount(“http://“, adapter) session.mount(“https://“, adapter) # 3. 设置合理的请求头模拟浏览器 session.headers.update({ ‘User-Agent‘: ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36‘, ‘Accept‘: ‘text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8‘, ‘Accept-Language‘: ‘en-US,en;q0.5‘, ‘Accept-Encoding‘: ‘gzip, deflate, br‘, ‘Connection‘: ‘keep-alive‘, }) return session # 使用示例 if __name__ ‘__main__‘: # 对于需要自定义证书的网站 session create_robust_session(ca_cert_path‘./my_ca.pem‘, retries5) # 对于公开网站使用默认验证 # session create_robust_session(retries5) try: response session.get(‘https://example.com‘, timeout10) response.raise_for_status() # 如果状态码不是200抛出HTTPError print(“请求成功“) # 处理响应内容... except requests.exceptions.SSLError as e: logger.error(f“SSL验证失败: {e}“) # 这里可以根据业务逻辑决定是否降级处理如verifyFalse或直接失败 except requests.exceptions.RequestException as e: logger.error(f“请求失败: {e}“)这个会话工厂函数的好处模块化将SSL配置、重试逻辑、请求头封装在一起。可重用在整个爬虫项目中可以创建多个不同配置的会话。健壮性通过Retry机制处理临时性网络错误和429状态码。可维护性所有网络相关配置集中在一处。5. 高级排查与调试技巧当以上“标准”方案都不奏效时需要更深入的排查手段。5.1 使用openssl命令行诊断OpenSSL工具链是诊断SSL/TLS问题的瑞士军刀。1. 模拟客户端连接查看完整证书链openssl s_client -connect example.com:443 -showcerts这个命令会输出服务器返回的所有证书。仔细检查输出是否有多段-----BEGIN CERTIFICATE-----这表示服务器发送了证书链。最后是否有一行Verify return code: 0 (ok)如果是说明你的本地OpenSSL验证通过了。问题可能出在Python的证书路径上。2. 检查Python使用的SSL库和默认路径import ssl import sys print(f“Python版本: {sys.version}“) print(f“OpenSSL版本: {ssl.OPENSSL_VERSION}“) paths ssl.get_default_verify_paths() print(f“默认CA文件: {paths.cafile}“) print(f“默认CA目录: {paths.capath}“) # 检查文件是否存在 import os if paths.cafile and os.path.exists(paths.cafile): print(f“CA文件存在大小: {os.path.getsize(paths.cafile)} 字节“) else: print(“CA文件不存在或路径为空“)5.2 深入requests/urllib3内部有时需要查看requests和底层urllib3到底在用什么。import requests import urllib3 # 查看requests使用的CA bundle路径受环境变量REQUESTS_CA_BUNDLE和CURL_CA_BUNDLE影响 print(“requests默认使用的CA bundle计算后:“) # requests内部通过requests.utils.DEFAULT_CA_BUNDLE_PATH确定但未直接暴露。 # 可以通过创建一个适配器来查看 from requests.adapters import HTTPAdapter adapter HTTPAdapter() pool adapter.init_poolmanager(connections1, maxsize1) # 查看poolmanager的SSL上下文较底层 print(pool.connection_pool_kw.get(‘ssl_context‘)) # 更直接的方法追踪一次实际请求启用调试日志输出会非常多 import logging logging.basicConfig(levellogging.DEBUG) # 然后运行一个requests请求会看到详细的握手和证书信息5.3 处理证书链不完整的情况如果openssl s_client显示服务器没有发送完整的证书链你可以尝试手动构建证书链。从服务器获取叶子证书openssl s_client -connect incomplete-chain.example.com:443 2/dev/null | sed -n ‘/-----BEGIN/,/-----END/p‘ server_cert.pem找到缺失的中间CA证书。你可以通过访问类似SSL Labs的在线测试工具或者如果知道证书颁发商如Let‘s Encrypt, DigiCert去其官网下载中间证书。将服务器证书和中间证书合并成一个文件cat server_cert.pem intermediate_ca.pem full_chain.pem在Python中使用这个合并后的文件进行验证。但请注意这通常不是客户端的责任应该要求服务器管理员正确配置证书链。5.4 终极“武器”自定义SSL上下文对于极其复杂的情况你可以直接操作Python的ssl模块创建完全自定义的SSL上下文然后交给requests使用。import ssl import requests from requests.adapters import HTTPAdapter from urllib3.poolmanager import PoolManager class CustomSSLContextAdapter(HTTPAdapter): “”“完全自定义SSL上下文的适配器”“” def __init__(self, ssl_contextNone, **kwargs): self._ssl_context ssl_context or ssl.create_default_context() super().__init__(**kwargs) def init_poolmanager(self, *args, **kwargs): kwargs[‘ssl_context‘] self._ssl_context return super().init_poolmanager(*args, **kwargs) # 创建一个自定义上下文加载特定的CA证书并调整验证选项 custom_context ssl.create_default_context(cafile‘./my-trust-store.pem‘) # 可以调整各种参数例如 # custom_context.check_hostname False # 不建议关闭主机名检查 # custom_context.verify_mode ssl.CERT_OPTIONAL # 修改验证模式 session requests.Session() adapter CustomSSLContextAdapter(ssl_contextcustom_context) session.mount(‘https://‘, adapter) response session.get(‘https://special-service.com‘)这种方法赋予了开发者最大的控制权但同时也要求对SSL/TLS有更深的理解错误配置会带来安全风险。6. 总结与最佳实践清单经过以上从原理到实战的梳理我们可以总结出一套处理Python中SSL: CERTIFICATE_VERIFY_FAILED错误的最佳实践永远不要在生产环境使用verifyFalse这是安全底线。将其限制在本地开发、调试和完全可控的测试环境。优先修复环境而非修改代码如果问题是缺失公共根证书第一时间在操作系统或容器镜像中安装ca-certificates包。这是最根本、最干净的解决方案。对自签名/私有证书使用verify参数指定证书文件将CA证书作为资源文件管理通过路径或环境变量传递给代码。这既安全又便于维护。善用certifi包在虚拟环境或对证书库有特定版本要求的场景下更新和使用certifi是可靠的选择。可以通过certifi.where()获取其路径。为爬虫或客户端程序配置健壮的会话使用requests.Session配合HTTPAdapter和Retry逻辑统一管理SSL验证、重试、超时和请求头提升代码的健壮性和可维护性。容器化部署时Dockerfile中必须安装CA证书这是CI/CD流程中必不可少的一步确保你的应用镜像在任何地方都能正常进行HTTPS通信。掌握基本的OpenSSL诊断命令openssl s_client是你的好朋友当出现问题时用它来检查服务器端的证书配置是否完整正确。理解错误信息unable to get local issuer certificate和certificate has expired代表不同的问题前者是信任链问题后者是有效期问题解决方法不同。最后SSL/TLS证书验证是互联网安全的基石。作为开发者我们遇到的每一个CERTIFICATE_VERIFY_FAILED错误都是一次深入理解这套安全机制的机会。正确处理它不仅能让你写的程序更稳定地运行也是在为构建更安全的网络环境尽一份力。下次再遇到这个错误时希望你能从容地选择最合适的那把“钥匙”而不是简单地砸掉“锁”verifyFalse。
返回列表