如何解决使用http.client.HTTPSConnection时的主机名不匹配错误?
问题背景
原有程序基于httplib/http.client实现请求发送,在Python2.7下运行正常,但切换到Python3后出现HTTPS连接异常:
- HTTP协议场景:Python2与Python3均能正常工作
- HTTPS协议场景:Python3连接部分命名服务器失败,抛出错误:
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for '{server name}'. (_ssl.c:1123)
运行环境覆盖:OS X Python3.9.6、AWS Linux Python3.8.5、Python2.7.18;IT部门为云内命名服务器配置了通配符证书,且已确认问题服务器的证书与Python3可正常连接的服务器证书一致。
相关代码片段:
模块导入
if PY_VERSION >= 3: import http.client as httplib else: import httplib
连接类型定义
if self.protocol == 'http': self.httpConn = httplib.HTTPConnection elif self.protocol == 'https': self.httpConn = httplib.HTTPSConnection
连接初始化
if self.protocol == 'https': self.ssl_context = ssl.create_default_context() self.the_conn = self.httpConn(self.site)
需求:明确Python2到Python3的行为变更原因,并提供兼容两个版本的解决方案(不可依赖requests模块,因Python2环境无该依赖)
问题核心原因
1. HTTPS主机名验证逻辑强制开启
Python2.7的httplib.HTTPSConnection默认不执行主机名验证——即使证书的CN(通用名称)或SAN(主题备用名称)与目标服务器域名不匹配,连接仍能成功建立。
而Python3.2+的http.client.HTTPSConnection默认强制开启主机名验证:使用ssl.create_default_context()创建的上下文会自动绑定主机名验证回调,严格校验证书中的域名是否与连接目标完全匹配。如果通配符证书不符合规范(比如*.example.com仅支持一级子域,而实际连接的是多级子域a.b.example.com),就会触发主机名不匹配错误。
2. SSL上下文默认配置差异
Python2中HTTPSConnection使用的默认SSL上下文未启用证书验证逻辑;Python3中ssl.create_default_context()默认启用CERT_REQUIRED验证模式,同时强制校验主机名与证书的匹配性。
兼容解决方案
方案1:自定义主机名验证逻辑(推荐,兼顾安全与兼容性)
针对Python3调整SSL上下文的验证规则,适配通配符证书的匹配需求;Python2保持原有逻辑或补充基础验证:
import ssl if PY_VERSION >= 3: import http.client as httplib else: import httplib # 初始化连接类型 if self.protocol == 'http': self.httpConn = httplib.HTTPConnection elif self.protocol == 'https': self.httpConn = httplib.HTTPSConnection # 建立连接 if self.protocol == 'https': if PY_VERSION >= 3: # 创建默认SSL上下文,自定义主机名验证规则 self.ssl_context = ssl.create_default_context() # 关闭默认验证回调,改用自定义逻辑 self.ssl_context.check_hostname = False self.ssl_context.verify_mode = ssl.CERT_REQUIRED self.the_conn = self.httpConn(self.site, context=self.ssl_context) self.the_conn.connect() # 自定义通配符匹配逻辑(支持多级子域场景) def custom_match_hostname(cert, hostname): from ssl import CertificateError # 优先取SAN中的域名,无则取CN cert_domains = [] if 'subjectAltName' in cert: cert_domains = [item[1] for item in cert['subjectAltName'] if item[0] == 'DNS'] else: cert_domains = [item[0][1] for item in cert['subject'] if item[0][0] == 'commonName'] for domain in cert_domains: if domain == hostname: return if domain.startswith('*.'): base_domain = domain[2:] # 匹配多级子域(如*.example.com 匹配 a.b.example.com) if hostname.endswith(f'.{base_domain}') or hostname == base_domain: return raise CertificateError(f"Hostname {hostname} does not match certificate domains {cert_domains}") # 手动执行主机名验证 custom_match_hostname(self.the_conn.sock.getpeercert(), self.site) else: # Python2环境保持原有连接逻辑,可选开启基础证书验证 self.the_conn = self.httpConn(self.site) else: self.the_conn = self.httpConn(self.site)
方案2:临时关闭主机名验证(仅内部可信环境使用)
若为完全可信的内部环境,可临时关闭主机名验证(注意:会降低连接安全性):
if self.protocol == 'https': if PY_VERSION >= 3: self.ssl_context = ssl.create_default_context() # 关闭主机名验证,保留证书链验证 self.ssl_context.check_hostname = False self.the_conn = self.httpConn(self.site, context=self.ssl_context) else: self.the_conn = self.httpConn(self.site) else: self.the_conn = self.httpConn(self.site)
补充说明
- 优先检查通配符证书是否符合RFC规范:标准通配符
*.example.com仅支持一级子域,若需匹配多级子域,证书需配置对应多级通配符(如*.b.example.com)或在SAN中添加具体的多级子域名。 - Python2.7.9及以上版本也支持通过
context参数传入SSL上下文,可进一步统一跨版本的验证逻辑。
内容的提问来源于stack exchange,提问作者Christopher Biessener

