You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

通过HTTPS直接使用IP访问共享主机文件时的SSL证书验证问题

通过HTTPS直接使用IP访问共享主机文件时的SSL证书验证问题

问题背景与现象

我在项目中遇到了这样的场景:数据存放在共享虚拟主机上,网站DNS托管在CloudFlare并启用了代理功能,之前通过域名HTTPS请求下载文件一切正常:

urllib.request.Request(
    'https://www.example.org/myfile.zip', 
    headers={...}
)

后来为了规避代理DNS的性能问题,我尝试直接用主机IP访问——因为是共享主机,必须在请求头中指定Host字段绑定对应域名。用HTTP协议时完全正常:

urllib.request.Request(
    "http://123.456.100.1/myfile.zip",
    headers={'Host': 'www.example.org'}
)

但换成HTTPS协议时,直接触发了SSL证书验证失败的错误:

urllib.error.URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: IP address mismatch, certificate is not valid for '123.456.100.1'

本质原因很明确:SSL证书是绑定域名的,直接用IP访问时,默认验证逻辑会检查证书是否对应该IP,自然会出现不匹配的情况。我们需要让SSL验证阶段使用Host头指定的域名,而非URL中的IP。


解决方案(基于urllib)

我们可以自定义一个HTTPSHandler,手动替换SSL验证时使用的主机名,覆盖默认的IP验证逻辑:

import urllib.request
import ssl

# 自定义HTTPS处理器,用于指定SSL验证的目标域名
class CustomHTTPSHandler(urllib.request.HTTPSHandler):
    def __init__(self, target_host):
        self.target_host = target_host
        # 初始化默认的安全SSL上下文
        ssl_context = ssl.create_default_context()
        super().__init__(context=ssl_context)

    def https_open(self, req):
        # 重写HTTPS请求逻辑,使用自定义的连接创建方法
        return self.do_open(self._create_ssl_connection, req)

    def _create_ssl_connection(self, host, timeout=300):
        # 创建SSL连接,但强制指定验证的主机名为目标域名(而非IP)
        conn = super().get_connection(host, timeout)
        # 覆盖连接的host属性,让SSL验证时用我们指定的域名
        conn.host = self.target_host
        return conn

# 1. 配置目标域名和IP
target_domain = "www.example.org"
host_ip = "123.456.100.1"

# 2. 创建带自定义处理器的Opener并安装
opener = urllib.request.build_opener(CustomHTTPSHandler(target_domain))
urllib.request.install_opener(opener)

# 3. 发起HTTPS请求,指定IP和Host头
req = urllib.request.Request(
    f"https://{host_ip}/myfile.zip",
    headers={'Host': target_domain}
)

# 4. 执行请求并保存文件
with urllib.request.urlopen(req) as response, open("myfile.zip", "wb") as f:
    f.write(response.read())

逻辑说明

这个自定义处理器的核心是:在创建SSL连接时,把原本要验证的IP地址替换成Host头对应的域名。这样SSL证书验证就会检查该域名是否在证书的有效范围内,和正常通过域名访问的验证逻辑完全一致,既解决了验证失败的问题,又保留了HTTPS的安全性。


更简洁的替代方案:使用requests库

如果项目允许切换到requests库,实现起来会更清爽,通过自定义SSL适配器即可完成需求:

import requests
import ssl
from requests.adapters import HTTPAdapter
from urllib3.poolmanager import PoolManager

class DomainVerifiedSSLAdapter(HTTPAdapter):
    def __init__(self, verified_domain, **kwargs):
        self.verified_domain = verified_domain
        super().__init__(**kwargs)

    def init_poolmanager(self, *args, **kwargs):
        # 初始化SSL连接池时,强制指定验证的主机名为目标域名
        ssl_context = ssl.create_default_context()
        kwargs["ssl_context"] = ssl_context
        kwargs["assert_hostname"] = self.verified_domain
        return super().init_poolmanager(*args, **kwargs)

# 1. 配置目标域名和IP
target_domain = "www.example.org"
host_ip = "123.456.100.1"

# 2. 创建会话并挂载自定义适配器
session = requests.Session()
session.mount("https://", DomainVerifiedSSLAdapter(target_domain))

# 3. 发起请求并保存文件
response = session.get(
    f"https://{host_ip}/myfile.zip",
    headers={'Host': target_domain}
)
with open("myfile.zip", "wb") as f:
    f.write(response.content)

重要提醒

绝对不要通过禁用SSL验证(比如设置ssl_context.check_hostname = False或verify=False)来绕开这个问题——这会完全丧失HTTPS的安全防护,让你的请求暴露在中间人攻击的风险下。我们的方案是保持完整的SSL验证逻辑,仅修正验证的目标主体,安全性和正常访问完全一致。

备注:内容来源于stack exchange,提问作者Johannes Hinrichs

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.14 11:09:33