1. 从一次“证书无效”的报错说起
最近在写一个自动化脚本,需要从公司内网一个自签了SSL证书的监控平台拉取数据。脚本很简单,用Python的requests库,几行代码就搞定了。然而,一运行就给我当头一棒,抛出了一个长长的SSLError,核心信息是[SSL: CERTIFICATE_VERIFY_FAILED],翻译过来就是SSL证书验证失败。这个错误对于经常和HTTPS打交道的开发者来说,简直是“老朋友”了。它背后代表的是requests库,或者说底层的urllib3,在严格履行其安全职责:验证你正在通信的服务器身份是否可信。当它发现服务器的SSL证书无法被信任(比如自签名、过期、域名不匹配)时,就会果断中止连接,防止你掉入中间人攻击的陷阱。
这个机制在访问公网正规网站时是完美的安全保障,但一旦遇到开发测试环境、内部系统、或者一些老旧设备的管理界面,自签名证书就成了常态。这时候,我们往往需要一种“权宜之计”,让程序能够暂时绕过严格的证书验证,先把数据拿到手再说。这就是“忽略SSL证书”需求的典型场景。同时,伴随而来的还有烦人的InsecureRequestWarning警告,虽然不影响程序运行,但满屏的黄色警告信息看着实在闹心,也需要一并关闭。
今天,我们就来彻底聊聊在Python的requests模块中,如何安全、正确地处理SSL证书验证,以及关闭相关警告。我会结合自己的踩坑经验,不仅告诉你“怎么做”,更会深入解释“为什么这么做”,以及在不同场景下的最佳实践和潜在风险。
2. 理解SSL证书验证与verify参数
在深入代码之前,我们必须先搞清楚requests(实际上是urllib3)在进行HTTPS请求时,到底做了什么。这个过程可以类比为你要进入一个高度机密的基地(服务器)。
- 出示证件(证书):当你(客户端)尝试连接基地(服务器)时,门卫(
requests库)会要求基地出示其官方证件(SSL证书)。 - 检查证件真伪(验证签名):门卫不会轻易相信一张白纸黑字的证件。他会检查这个证件是否由他信任的“发证机关”(Certificate Authority, CA)签发。你的操作系统或Python环境里预装了一份“可信发证机关名单”(CA信任根证书库)。门卫会用这份名单里的“公章”去核验证件上的签名。
- 检查证件信息(验证域名/有效期):确认证件是真的之后,门卫还要检查证件上的照片、姓名(证书中的Common Name或Subject Alternative Names)是否与你声称要访问的基地名称(请求的URL域名)一致。同时检查证件是否在有效期内。
- 放行或拦截:只有以上所有检查都通过,门卫才会放行,建立一条加密的、可信的通信通道。任何一步失败,门卫都会立即拦截并报告错误(抛出
SSLError)。
在requests中,控制这个“门卫”严格与否的核心参数就是verify。它默认值为True,意味着执行上述全套严格检查。
import requests # 默认行为,进行完整的SSL证书验证 response = requests.get('https://example.com') # 如果example.com证书无效,这里会报错当我们将verify设置为False时,就相当于命令门卫:“别检查了,直接放行”。这会跳过所有证书验证步骤。
import requests # 忽略所有SSL证书验证 response = requests.get('https://self-signed.badssl.com', verify=False)注意:
verify=False是极不安全的操作。它使你的连接暴露在中间人攻击的风险之下。攻击者可以轻易地冒充目标服务器,窃取或篡改你传输的敏感数据(如密码、令牌、个人信息)。因此,这绝对不应用于生产环境或处理任何敏感信息的场景。它仅应作为开发、测试、或访问完全可控的内部环境时的临时解决方案。
3. 实战:忽略证书验证的多种方法
了解了原理,我们来看看具体怎么操作。根据不同的使用场景,有几种不同的方法。
3.1 单次请求级别忽略
这是最常用、最灵活的方式。通过给requests.get(),requests.post()等方法传递verify=False参数,仅对当前这次请求忽略证书验证。
import requests url = "https://internal-server.company.local/api/data" payload = {"key": "value"} headers = {"Content-Type": "application/json"} # 针对这一次POST请求,忽略SSL证书验证 try: response = requests.post(url, json=payload, headers=headers, verify=False) print(response.status_code) print(response.json()) except requests.exceptions.SSLError as e: print(f"SSL错误发生: {e}")这种方法的好处是影响范围最小,只针对特定的、已知有证书问题的请求。代码意图清晰。
3.2 会话级别忽略
如果你需要使用requests.Session()对象来保持会话(如处理cookies、保持连接池),并且这个会话需要访问多个使用自签名证书的端点,那么在会话级别设置verify=False会更方便。
import requests # 创建一个会话 session = requests.Session() # 为该会话的所有请求关闭SSL验证 session.verify = False # 现在,通过这个会话发起的请求都不会验证证书 response1 = session.get('https://internal-api.local/users') response2 = session.post('https://internal-api.local/auth', data={'user': 'admin'}) # 注意:直接使用 requests.get() 不受此会话影响,它仍会验证证书。3.3 全局忽略(极其不推荐)
通过修改requests库的默认配置,可以全局关闭SSL验证。强烈不建议这样做,因为它会影响你整个Python进程中所有使用requests的代码,包括你可能依赖的第三方库,带来不可预知的安全风险。
import requests import urllib3 # 禁用全局的SSL警告(先做这个,否则会有警告) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # 修改requests的默认行为(危险操作!) requests.packages.urllib3.disable_warnings() # 另一种禁用警告的方式 # 实际上,requests库没有直接的全局verify设置。通常是通过环境变量或修改默认会话。 # 更危险的做法是猴子补丁,这里仅作演示,切勿在生产环境使用。 original_request = requests.request def insecure_request(*args, **kwargs): kwargs['verify'] = False return original_request(*args, **kwargs) requests.request = insecure_request # 此后,所有 requests.request 及其衍生方法(get, post)都将 verify=False再次强调,除非你在一个完全隔离的、无网络风险的测试环境中,否则请避免使用全局修改的方法。
4. 处理烦人的InsecureRequestWarning警告
当你使用verify=False时,urllib3(requests底层使用的HTTP库)会出于安全考虑,抛出一个InsecureRequestWarning。这是一个警告(Warning),不是错误(Exception),所以你的程序会继续运行,但控制台会输出类似下面的内容:
/usr/local/lib/python3.9/site-packages/urllib3/connectionpool.py:1045: InsecureRequestWarning: Unverified HTTPS request is being made to host 'internal-server.company.local'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html#ssl-warnings warnings.warn(对于自动化脚本或希望保持输出整洁的场景,我们需要关闭这个警告。
4.1 通过urllib3禁用特定警告
最标准、最推荐的做法是使用urllib3本身提供的方法来禁用InsecureRequestWarning。
import requests import urllib3 # 禁用 InsecureRequestWarning 警告 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # 现在,即使 verify=False,也不会打印警告 response = requests.get('https://self-signed.badssl.com', verify=False) print("请求成功,无警告信息")4.2 通过requests的封装方法禁用
requests库也提供了一个便捷的封装,效果与上面相同。
import requests # 使用 requests 封装的禁用警告方法 requests.packages.urllib3.disable_warnings(category=requests.packages.urllib3.exceptions.InsecureRequestWarning) response = requests.get('https://self-signed.badssl.com', verify=False)4.3 使用Python的warnings模块全局过滤
你也可以使用Python标准库的warnings模块进行更全局的控制。这种方法可以过滤掉所有地方的InsecureRequestWarning,但需要知道完整的警告类路径。
import warnings import requests # 从urllib3导入具体的警告类 from urllib3.exceptions import InsecureRequestWarning # 过滤掉特定的警告 warnings.filterwarnings('ignore', category=InsecureRequestWarning) response = requests.get('https://self-signed.badssl.com', verify=False)4.4 临时重定向警告输出
如果你只想在特定代码块中抑制警告,而不是全局禁用,可以使用warnings.catch_warnings上下文管理器。这是更精细的控制方式。
import warnings import requests with warnings.catch_warnings(): # 在这个代码块内,忽略所有警告(或可指定类别) warnings.simplefilter("ignore") # 或者只忽略 InsecureRequestWarning # from urllib3.exceptions import InsecureRequestWarning # warnings.filterwarnings("ignore", category=InsecureRequestWarning) response = requests.get('https://self-signed.badssl.com', verify=False) print("代码块内请求完成") # 代码块外,警告恢复原有行为 response2 = requests.get('https://self-signed.badssl.com', verify=False) # 这里又会有警告了个人建议:在脚本的开头,使用urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)是最清晰直接的做法。如果脚本是工具类,可能会被他人复用,可以在函数内部使用上下文管理器warnings.catch_warnings进行局部抑制,避免影响调用者的环境。
5. 更优方案:使用自定义CA证书或私有证书
完全忽略验证是下策。在多数企业内部场景下,存在一个更安全、更规范的解决方案:使用自定义的CA证书。
很多公司会有自己的内部CA(证书颁发机构),为所有内部服务器签发证书。你只需要将公司内部CA的根证书(通常是一个.crt或.pem文件)添加到你的信任链中,requests就能像信任公共CA一样信任这些内部证书。
5.1 为单个请求指定CA证书包
verify参数不仅可以接受布尔值,还可以接受一个字符串路径,指向一个包含CA证书的文件(通常是.pem格式),或者一个包含多个证书的目录。
import requests # 假设你从公司IT部门拿到了内部CA的证书文件 internal_ca.pem ca_cert_path = "/path/to/your/internal_ca.pem" # 将 verify 指向你的CA证书文件 response = requests.get('https://internal-service.company.local', verify=ca_cert_path)这样,requests在验证internal-service.company.local的证书时,不仅会检查系统默认的信任库,还会检查你提供的internal_ca.pem。如果服务器证书是由这个内部CA签发的,验证就会通过。
5.2 将私有CA证书添加到系统或环境信任库
对于长期需要访问大量内部服务的开发机,更一劳永逸的方法是将内部CA证书安装到操作系统或Python环境的默认信任库中。这样,所有使用系统CA库的程序(包括requests)都会自动信任这些内部证书。
Linux/Mac (使用系统CA存储):通常可以将.pem证书文件复制到/usr/local/share/ca-certificates/目录,然后运行更新命令。
# 将证书复制到指定目录 sudo cp internal_ca.pem /usr/local/share/ca-certificates/ # 更新系统CA证书存储 sudo update-ca-certificates之后,Python的requests(通过ssl模块)就会自动识别这个新加入的CA。
Windows:可以通过证书管理器(certmgr.msc)将CA证书导入到“受信任的根证书颁发机构”存储中。
通过环境变量指定(跨平台):你可以设置REQUESTS_CA_BUNDLE环境变量,指向一个自定义的CA证书包文件。
# Bash export REQUESTS_CA_BUNDLE=/path/to/your/ca_bundle.pem # 或者在Python脚本中设置 import os os.environ['REQUESTS_CA_BUNDLE'] = '/path/to/your/ca_bundle.pem'设置后,requests会优先使用这个环境变量指定的证书包,而不是系统默认的。
5.3 创建自定义的CA证书包文件
如果你的环境混合了多个私有CA,或者你需要精确控制信任哪些CA,可以自己创建一个CA证书包文件(.pem格式)。这个文件其实就是将多个CA证书(PEM格式)简单地拼接在一个文件里。
# 将多个CA证书合并成一个bundle文件 cat ca_cert1.pem ca_cert2.pem > my_custom_ca_bundle.pem然后在requests中使用这个bundle文件:
import requests response = requests.get('https://some.internal.site', verify='/path/to/my_custom_ca_bundle.pem')6. 深入排查:当verify=False仍报错时
有时候,即使你设置了verify=False,仍然可能遇到与SSL/TLS相关的错误。这通常不是证书验证本身的问题,而是更深层的协议或配置问题。
6.1 协议版本不匹配
服务器可能只支持老旧的、不安全的TLS协议版本(如TLSv1.0),而现代Python环境默认可能已禁用这些版本。或者反过来,客户端环境太老,不支持服务器要求的新协议。
解决方案:在requests请求中指定协议版本。
import requests import ssl from requests.adapters import HTTPAdapter from urllib3.poolmanager import PoolManager # 方法1:为特定会话适配所有请求(推荐) class LegacyTLSAdapter(HTTPAdapter): def init_poolmanager(self, *args, **kwargs): # 强制使用 TLSv1.2,可以根据需要改为 ssl.PROTOCOL_TLSv1, ssl.PROTOCOL_TLSv1_1 等 kwargs['ssl_version'] = ssl.PROTOCOL_TLSv1_2 return super().init_poolmanager(*args, **kwargs) session = requests.Session() session.mount('https://', LegacyTLSAdapter()) session.verify = False # 如果需要的话 response = session.get('https://old-server.local') # 方法2:更暴力的全局SSL上下文设置(不推荐,影响广泛) import urllib3 # 创建使用特定协议版本的HTTPS连接池管理器(示例) # 这需要更底层的操作,通常建议使用方法1。6.2 密码套件不匹配
服务器可能只支持某些特定的、客户端不支持的加密套件。
解决方案:这个问题较难在requests高层解决,通常需要调整系统或Python的SSL配置。一个尝试方向是使用CURL或openssl客户端测试服务器支持的套件,确认问题。在极端情况下,可能需要像上面一样,通过自定义HTTPAdapter来配置底层的ssl.SSLContext,设置特定的密码套件。
6.3 证书格式问题(即使verify=False)
在某些非常特殊的情况下,服务器返回的证书本身格式就是畸形的,导致SSL握手在验证之前就失败了。verify=False跳过了“验证”,但没跳过“解析”。如果证书不是有效的X.509格式,连接依然会失败。
排查方法:使用openssl s_client命令进行诊断。
openssl s_client -connect your-server.com:443 -showcerts观察输出,看是否能正常接收到证书。如果这里就报错,那问题出在服务器端。
6.4 其他网络层问题
错误信息可能被包装在SSLError中,但根本原因可能是网络代理、防火墙拦截了TLS握手包,或者服务器端口根本不是HTTPS。
排查思路:
- 先用浏览器或
curl访问同一地址,确认服务本身可用。 - 检查是否有系统代理或环境变量(如
HTTP_PROXY,HTTPS_PROXY)干扰。requests默认会使用这些代理设置。 - 尝试在请求中显式设置
proxies参数,或将其设置为None来绕过代理。
import requests # 尝试不使用代理 proxies = { "http": None, "https": None, } response = requests.get('https://target.com', verify=False, proxies=proxies)7. 安全实践与经验总结
在开发中绕过SSL验证是常见操作,但务必牢记安全边界。以下是我总结的一些经验原则:
环境隔离原则:将
verify=False的代码严格限制在开发、测试或完全可信的隔离内网环境中。可以通过环境变量来控制这一行为。import os import requests DEV_MODE = os.getenv('ENVIRONMENT') == 'development' # 或者使用自定义配置 SKIP_SSL_VERIFY = os.getenv('SKIP_SSL_VERIFY', 'false').lower() == 'true' session = requests.Session() if DEV_MODE or SKIP_SSL_VERIFY: import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) session.verify = False else: # 生产环境使用严格验证,或指定特定的CA bundle # session.verify = '/path/to/prod/ca-bundle.crt' pass # 使用 session 进行请求证书固定(Certificate Pinning):对于特别重要的客户端(如移动App、关键服务客户端),可以考虑证书固定。这不是完全忽略验证,而是只信任一个或几个特定的证书(或公钥),而不是整个CA体系。
requests本身不直接支持,但可以通过自定义HTTPAdapter和ssl.SSLContext实现,或使用requests的cert参数进行客户端证书双向验证(这是另一种场景)。警告处理要到位:在脚本中禁用
InsecureRequestWarning是一个好习惯,可以保持输出清晰。但最好在脚本开头用日志或打印语句明确说明“SSL验证已禁用”,让后续维护者清楚潜在风险。优先使用私有CA:只要条件允许,永远优先选择将内部CA证书加入信任链的方案(
verify=/path/to/ca.pem),而不是完全关闭验证。这是兼顾安全与便利的最佳实践。理解错误根源:遇到SSL错误不要习惯性地
verify=False了事。先花几分钟用openssl s_client或浏览器检查一下证书的详细信息(颁发者、有效期、域名匹配)。很多时候,可能是服务器证书配置错误(如用了IP地址但证书里是域名),修复服务器配置才是根本解决之道。
处理SSL证书问题就像处理一扇门的安全锁。verify=False是直接把锁拆了,方便但危险。而使用正确的钥匙(自定义CA证书)或者调整锁具(协议/套件配置),才能在安全与便利之间找到平衡点。希望这些具体的代码和排查思路,能让你下次再遇到CERTIFICATE_VERIFY_FAILED时,可以从容地选择最合适的解决方案。