Email:2225994292@qq.com
CNY
Python requests库中如何校验SSL证书有效性?
更新时间:2025-11-07 作者:SSL证书

开发者在使用requests时,对 “如何手动校验证书”“如何解决证书验证失败” 等关键问题缺乏深入理解,可能导致安全漏洞或请求异常。本文将从requests库的 SSL 处理原理出发,详细讲解证书有效性校验的实现方法、常见场景与问题解决方案,帮助开发者构建安全可靠的 HTTPS 请求逻辑。

一、requests库的默认 SSL 处理机制:为何有时会 “自动通过” 或 “报错”?

在深入手动校验前,需先理解requests库默认的SSL证书处理逻辑 —— 这是后续自定义校验的基础,也是解决 “为何有时请求成功,有时报 SSL 错误” 的关键。

1. 默认行为:信任系统根证书,自动校验证书链

requests库基于urllib3实现底层网络通信,其默认的 SSL 校验逻辑遵循以下规则:

  • 信任根证书来源:requests默认信任操作系统或 Python 环境中的 “根证书集合”(如 Windows 的 “受信任的根证书颁发机构”、Linux 的/etc/ssl/certs目录、macOS 的钥匙串),这些根证书由全球权威 CA(如 Let’s Encrypt、DigiCert)颁发,是SSL证书信任链的 “起点”;
  • 自动校验证书链完整性:当发送 HTTPS 请求时,requests会自动获取目标服务器返回的 “服务器证书”“中间证书”,并验证其是否能通过根证书 “链式推导” 形成完整信任链 —— 若链条完整且所有证书未过期、未被吊销,校验通过;否则,抛出SSLError异常;
  • 校验核心维度:默认校验包含四个核心维度:

a. 证书是否在有效期内(未过期且未提前生效);

b. 证书的 “主题备用名称(SAN)” 是否与请求的域名匹配(防止域名劫持);

c. 证书链是否完整(包含服务器证书、中间证书,且能追溯至根证书);

d. 证书是否被列入 “证书吊销列表(CRL)” 或通过 “在线证书状态协议(OCSP)” 验证为有效(部分环境默认开启)。

例如,当使用requests.get("https://www.baidu.com")发送请求时,requests会自动校验百度服务器的SSL证书:验证其 SAN 包含 “baidu.com”,证书链能追溯至 DigiCert 根证书,且证书未过期,最终校验通过,返回响应。

2. 默认校验失败的常见场景

若目标服务器的SSL证书不符合上述校验规则,requests会抛出requests.exceptions.SSLError异常,常见场景包括:

场景 1:自签名证书服务器使用 OpenSSL 等工具生成的自签名证书(未经过权威 CA 签名),因不在requests默认信任的根证书列表中,校验失败;

场景 2:证书链不完整:服务器仅配置了 “服务器证书”,未提供 “中间证书”,导致requests无法从服务器证书追溯至根证书,校验失败;

场景 3:域名不匹配:证书的 SAN 中未包含请求的域名(如证书为 “example.com”,但请求 “test.example.com”);

场景 4:证书过期或吊销:证书已超过有效期,或因私钥泄露等原因被 CA 吊销;

场景 5:弱协议 / 加密套件:服务器仅支持 SSLv3、TLS 1.0 等存在漏洞的协议,或使用强度不足的加密套件,requests出于安全考虑拒绝建立连接。

例如,访问使用自签名证书的本地测试服务器时,执行requests.get("https://127.0.0.1:8443")会抛出类似以下的错误:

1   requests.exceptions.SSLError: HTTPSConnectionPool(host='127.0.0.1', port=8443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1131)')))

二、requests库中SSL证书有效性校验的三种核心方式

根据实际开发需求(如测试环境、生产环境、私有服务访问),requests提供了三种不同的SSL证书校验方式,从 “完全默认” 到 “高度自定义”,覆盖不同安全等级的场景。

方式一:默认校验(推荐生产环境)—— 无需额外配置,依赖系统根证书

适用场景:访问公网中使用权威 CA 颁发证书的服务(如百度、谷歌、GitHub 等),无需自定义校验逻辑,追求最高安全性。

实现方法:直接使用requests的get/post等方法,无需添加任何 SSL 相关参数,requests会自动完成证书校验。

代码示例:

1    importrequests
2
3    # 访问使用权威CA证书的服务,默认校验SSL证书
4    try:
5        response =requests.get("https://github.com")
6        print("请求成功,状态码:", response.status_code)  # 输出200表示校验通过
7    exceptrequests.exceptions.SSLError as e:
8        print("SSL证书校验失败:", str(e))

优势与注意事项:

  • 优势:零配置、高安全,自动适配系统根证书更新(如 CA 发布新根证书后,系统更新后requests会自动信任);
  • 注意事项:

a. 若系统根证书未更新(如旧版 Linux 未安装 Let’s Encrypt 的根证书),可能导致对使用 Let’s Encrypt 证书的服务校验失败,需手动更新系统根证书(如 Ubuntu 执行sudo apt update && sudo apt install ca-certificates);

b. 生产环境中禁止关闭默认校验(如设置verify=False),否则会面临中间人攻击风险。

方式二:手动指定信任的证书文件(推荐测试 / 私有环境)—— 仅信任特定证书

适用场景:访问使用自签名证书或私有 CA(如企业内部 CA)颁发证书的服务(如本地测试服务器、企业内网 API),需明确指定 “可信任的证书”,避免信任系统所有根证书。

实现原理:通过verify参数指定本地的 “证书文件路径”(支持 PEM 格式),requests会仅信任该证书(或证书文件中的所有证书),并基于此验证服务器证书的有效性。

1. 前置准备:获取目标服务器的证书文件

首先需从目标服务器获取证书文件(PEM 格式),常用方法有两种:

方法 1:通过 OpenSSL 命令提取:

在终端执行以下命令,从服务器提取证书并保存为server.crt文件(以本地服务器127.0.0.1:8443为例):

1    openssl s_client -connect 127.0.0.1:8443 -showcerts < /dev/null 2>/dev/null | openssl x509 -outform PEM > server.crt

该命令会建立与服务器的 SSL 连接,提取服务器证书并转换为 PEM 格式保存。

方法 2:从浏览器导出:

在 Chrome/Firefox 中访问目标服务器,点击地址栏的 “安全锁”→“证书”→“详细信息”→“复制到文件”,选择 “Base64 编码 X.509 (.CER)” 格式(与 PEM 格式兼容),保存为server.crt

2. 代码实现:指定证书文件进行校验

将获取的server.crt文件放在项目目录中,通过verify参数指定其路径,requests会基于该证书校验服务器证书。

代码示例:

1    importrequests
2
3    # 手动指定信任的证书文件路径(相对路径或绝对路径)
4    cert_file = "server.crt"
5
6    try:
7        # verify参数指定证书文件,requests仅信任该证书
8        response =requests.get("https://127.0.0.1:8443", verify=cert_file)
9        print("请求成功,响应内容:", response.text[:100])  # 输出前100字符表示校验通过
10  exceptrequests.exceptions.SSLError as e:
11      print("SSL证书校验失败:", str(e))

3. 特殊场景:信任多个证书(证书链 / CA 证书)

若目标服务器的证书链包含多个中间证书,或需信任整个私有 CA 的证书(而非单个服务器证书),可将所有需信任的证书(服务器证书、中间证书、CA 根证书)合并到同一个 PEM 文件中,verify参数指定该合并文件即可。

合并证书方法:

用文本编辑器打开所有 PEM 格式的证书文件,按 “服务器证书→中间证书→CA 根证书” 的顺序拼接,保存为trusted_certs.pem(每个证书需完整包含-----BEGIN CERTIFICATE----------END CERTIFICATE-----标记)。

代码示例:

1    # 信任多个证书(合并后的证书文件)
2    response =requests.get("https://private-api.company.com", verify="trusted_certs.pem")

方式三:双向 SSL 校验(高级场景)—— 客户端与服务器互相验证证书

适用场景:高安全需求场景(如金融交易、企业核心 API),不仅需要客户端校验服务器证书,服务器也需校验客户端证书,确保双方身份均合法(即 “双向认证”)。

实现原理:

  • 客户端需准备 “客户端证书”(由服务器信任的 CA 颁发,通常包含证书文件client.crt和私钥文件client.key);
  • 通过cert参数将客户端证书与私钥传递给requests,服务器会验证客户端证书的有效性;
  • 同时通过verify参数指定服务器的信任证书(或 CA 证书),客户端验证服务器证书。

1. 前置准备:获取客户端证书与私钥

客户端证书通常由服务提供方(如企业 IT 部门、API 服务商)颁发,格式为 PEM 或 PFX:

  • 若为 PEM 格式,会获得两个文件:client.crt(客户端证书)和client.key(客户端私钥);
  • 若为 PFX 格式(包含证书与私钥的打包文件),需通过 OpenSSL 转换为 PEM 格式:
1    # 将PFX文件转换为PEM格式的证书和私钥
2    openssl pkcs12 -in client.pfx -clcerts -nokeys -out client.crt
3    openssl pkcs12 -in client.pfx -nocerts -out client.key -nodes  # -nodes表示不加密私钥

2. 代码实现:双向 SSL 校验

通过cert参数传递客户端证书与私钥的路径(元组形式:(cert_file, key_file)),verify参数指定服务器的信任证书,实现双向校验。

代码示例:

1    importrequests
2
3    # 客户端证书与私钥路径(PEM格式)
4    client_cert = ("client.crt", "client.key")
5    # 服务器信任证书路径(可是服务器证书或CA证书)
6    server_cert = "server_ca.crt"
7
8    try:
9        # cert参数:客户端证书与私钥;verify参数:校验服务器证书
10      response =requests.get(
11          "https://secure-api.bank.com/transaction",
12          cert=client_cert,
13          verify=server_cert
14      )
15      print("双向SSL校验成功,交易数据:", response.json())
16  exceptrequests.exceptions.SSLError as e:
17      print("双向SSL校验失败:", str(e))
18  except Exception as e:
19      print("请求异常:", str(e))

注意事项:

  • 客户端私钥需确保安全存储(如避免硬编码在代码中、使用加密存储),防止私钥泄露导致身份伪造;
  • 若服务器使用自签名 CA 颁发证书,verify参数需指定该 CA 的根证书,而非单个服务器证书。

三、常见问题与解决方案:从 “校验失败” 到 “安全请求”

在使用requests校验SSL证书时,开发者常遇到 “校验失败”“私钥加密”“协议不兼容” 等问题,以下是高频问题的解决方案:

问题 1:自签名证书校验失败(SSLCertVerificationError)

错误表现:

1   requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1131)

解决方案:

  • 场景 1:测试环境:通过verify参数指定自签名证书文件(参考 2.2 节),而非关闭校验;
  • 场景 2:临时调试(不推荐生产):若仅为临时调试,可设置verify=False关闭 SSL 校验(但会面临安全风险,需在代码中明确标注风险):
1    # 临时关闭SSL校验(生产环境禁止使用)
2    response =requests.get("https://127.0.0.1:8443", verify=False)
3    # 关闭requests的警告(可选,避免控制台输出安全警告)
4   requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
  • 注意:生产环境中绝对禁止verify=False,否则黑客可通过中间人攻击窃取请求数据(如账号密码、业务数据)。

问题 2:证书链不完整(unable to get local issuer certificate)

错误表现:

1   requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1131)

原因:服务器仅返回 “服务器证书”,未返回 “中间证书”,requests无法从服务器证书追溯至根证书。

解决方案:

方法 1:服务器端修复(推荐):在服务器配置中添加完整的证书链(将服务器证书与中间证书合并为一个文件,如 Nginx 的ssl_certificate参数指定合并后的文件);

方法 2:客户端补充中间证书:从 CA 官网下载对应的中间证书(如 Let’s Encrypt 的中间证书可从其官网获取),与服务器证书合并为trusted_certs.pemverify参数指定该文件(参考 2.2.3 节)。

问题 3:客户端私钥被加密(bad decrypt)

错误表现:

1   requests.exceptions.SSLError: [SSL: BAD_DECRYPT] bad decrypt (_ssl.c:2633)

原因:客户端私钥文件(client.key)在生成时被加密(如 OpenSSL 转换时未加-nodes参数),requests无法读取加密的私钥。

解决方案:

方法 1:解密私钥:在终端通过 OpenSSL 解密私钥,生成未加密的私钥文件:

1    openssl rsa -in encrypted_client.key -out client.key  # 执行后输入私钥的加密密码

方法 2:代码中指定私钥密码(进阶):若私钥必须加密存储,可通过ssl模块手动构建 SSL 上下文,传入私钥密码,再传递给 requests:

1    importrequests
2    import ssl
3    fromrequests.adapters import HTTPAdapter
4    from urllib3.poolmanager import PoolManager
5    from urllib3.util.ssl_ import create_ssl_context
6
7    # 自定义SSL上下文,指定私钥密码
8    class SSLAdapter(HTTPAdapter):
9        def __init__(self, ssl_context=None, **kwargs):
10          self.ssl_context = ssl_context
11          super().__init__(** kwargs)
12  
13      def init_poolmanager(self, connections, maxsize, block=False):
14          self.poolmanager = PoolManager(
15              num_pools=connections,
16              maxsize=maxsize,
17              block=block,
18              ssl_context=self.ssl_context
19          )
20
21  # 创建支持私钥密码的SSL上下文
22  context = create_ssl_context()
23  # 加载客户端证书与加密私钥,传入密码(b"password"为私钥的加密密码)
24  context.load_cert_chain(certfile="client.crt", keyfile="encrypted_client.key", password=b"your_key_password")
25
26  # 为requests会话绑定自定义SSL上下文
27  s =requests.Session()
28  s.mount("https://", SSLAdapter(context=context))
29
30  # 发送请求(无需再指定cert参数)
31  response = s.get("https://secure-api.bank.com", verify="server_ca.crt")
32  print(response.status_code)

问题 4:协议 / 加密套件不兼容(sslv3 alert handshake failure)

错误表现:

1   requests.exceptions.SSLError: [SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:1131)

原因:服务器仅支持旧版协议(如 TLS 1.0)或特定加密套件,而requests默认禁用不安全的旧协议,导致握手失败。

解决方案:

  • 优先方案:升级服务器配置,启用 TLS 1.2/1.3 协议与安全的加密套件(如 ECDHE-RSA-AES256-GCM-SHA384),避免使用 SSLv3、TLS 1.0;
  • 临时兼容方案(仅用于旧服务器):通过ssl模块手动启用旧协议(需注意:启用旧协议存在安全风险,如 TLS 1.0 易受 BEAST 攻击):
1    importrequests
2    import ssl
3    fromrequests.adapters import HTTPAdapter
4    from urllib3.poolmanager import PoolManager
5    from urllib3.util.ssl_ import create_ssl_context
6 
7    # 自定义Adapter,启用TLS 1.0(仅用于兼容旧服务器)
8    class TLS10Adapter(HTTPAdapter):
9        def init_poolmanager(self, connections, maxsize, block=False):
10          context = create_ssl_context(ssl.PROTOCOL_TLSv1)  # 启用TLS 1.0
11          # 可选:指定服务器支持的加密套件
12          context.set_ciphers("ECDHE-RSA-AES256-SHA")
13          self.poolmanager = PoolManager(
14              num_pools=connections,
15              maxsize=maxsize,
16              block=block,
17              ssl_context=context
18          )
19
20  s =requests.Session()
21  s.mount("https://", TLS10Adapter())  # 为HTTPS请求绑定TLS 1.0 Adapter
22
23  # 发送请求
24  response = s.get("https://old-server.example.com", verify="server.crt")
25  print(response.status_code)

四、进阶:自定义 SSL 校验逻辑(基于 ssl 模块)

对于复杂场景(如自定义证书吊销校验、动态信任证书),仅靠requests的verifycert参数无法满足需求,需通过 Python 内置的ssl模块手动构建 SSL 上下文,实现高度自定义的校验逻辑。

1. 核心思路:构建自定义 SSLContext

ssl.SSLContext是 Python 中控制 SSL 行为的核心类,通过它可实现:

  • 自定义信任的根证书;
  • 启用 / 禁用特定协议与加密套件;
  • 自定义证书验证回调函数(如自定义吊销校验、域名匹配规则)。

2. 代码示例:自定义证书验证回调

以下示例通过ssl.SSLContextverify_modecheck_hostname参数,结合自定义验证回调函数,实现 “仅信任特定域名且证书未过期” 的校验逻辑。

1    importrequests
2    import ssl
3    fromrequests.adapters import HTTPAdapter
4    from urllib3.poolmanager import PoolManager
5    from urllib3.util.ssl_ import create_ssl_context
6    from datetime import datetime
7
8    def custom_verify_callback(conn, cert, errno, depth, ok):
9        """
10      自定义证书验证回调函数
11      :param conn: SSL连接对象
12      :param cert: 证书信息(字典格式,包含subject、expire_date等)
13      :param errno: 错误码(0表示无错误)
14      :param depth: 证书在链中的深度(0表示服务器证书)
15      :param ok: 前序验证结果(True/False)
16      :return: 最终验证结果(True/False)
17      """
18      # 仅验证服务器证书(depth=0)
19      if depth != 0:
20          return ok
21  
22      # 1. 校验证书是否在有效期内
23      expire_date = datetime.strptime(cert["notAfter"], "%b %d %H:%M:%S %Y %Z")
24      if expire_date < datetime.utcnow():
25          print(f"证书已过期,过期时间:{expire_date}")
26          return False
27
28      # 2. 校验证书域名是否为预期域名(如"api.example.com")
29      expected_domain = "api.example.com"
30      # 从证书的subjectAltName中提取域名
31      san = cert.get("subjectAltName", [])
32      cert_domains = [name for (type_, name) in san if type_ == "DNS"]
33      # 若未设置SAN,从subject的CN字段提取域名
34      if not cert_domains:
35          cn = [item[0][1] for item in cert["subject"] if item[0][0] == "commonName"][0]
36          cert_domains = [cn]
37      # 检查预期域名是否在证书域名列表中
38      if expected_domain not in cert_domains:
39          print(f"证书域名不匹配,预期:{expected_domain},实际:{cert_domains}")
40          return False
41
42      # 3. 其他自定义校验(如校验证书颁发者、吊销状态等)
43      # ...
44
45      return True
46
47  # 构建自定义SSL上下文
48  context = create_ssl_context(ssl.PROTOCOL_TLSv1_2)  # 仅启用TLS 1.2
49  # 加载信任的CA证书(或服务器证书)
50  context.load_verify_locations("trusted_ca.crt")
51  # 启用证书验证
52  context.verify_mode = ssl.CERT_REQUIRED
53  # 禁用自动域名校验(由自定义回调处理)
54  context.check_hostname = False
55  # 设置自定义验证回调函数
56  context.set_verify_mode(ssl.CERT_REQUIRED)
57  context.set_default_verify_paths()
58  ssl.verify_callback = custom_verify_callback  # 注意:此处需将回调绑定到ssl模块
59
60  # 为requests会话绑定自定义上下文
61  class CustomSSLAdapter(HTTPAdapter):
62      def init_poolmanager(self, connections, maxsize, block=False):
63          self.poolmanager = PoolManager(
64              num_pools=connections,
65              maxsize=maxsize,
66              block=block,
67              ssl_context=context
68          )
69
70  s =requests.Session()
71  s.mount("https://", CustomSSLAdapter())
72
73  # 发送请求
74  try:
75      response = s.get("https://api.example.com/data")
76      print("自定义校验成功,响应:", response.text[:100])
77  exceptrequests.exceptions.SSLError as e:
78      print("自定义校验失败:", str(e))

上述文章详细覆盖了requests库校验SSL证书的多种场景与问题解决方法,若你在实际开发中遇到特定场景(如结合框架使用、处理特殊证书格式),或有进一步优化需求,可随时告知,我会为你补充更针对性的内容。


Dogssl.cn拥有20年网络安全服务经验,提供构涵盖国际CA机构SectigoDigicertGeoTrustGlobalSign,以及国内CA机构CFCA沃通vTrus上海CA等数十个SSL证书品牌。全程技术支持及免费部署服务,如您有SSL证书需求,欢迎联系!
相关文档
立即加入,让您的品牌更加安全可靠!
申请SSL证书
0.170462s