错误信息
Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate
也可能输出为
- requests.exceptions.SSLError: HTTPSConnectionPool(host='api.example.com', port=8443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate
主机、端口和 URL 路径取自你的请求。最内层的原因各不相同(“self-signed certificate in certificate chain”、“Hostname mismatch…”、“certificate has expired”),这一行以源代码位置结尾,例如“(_ssl.c:1082)')))”。
requests / urllib3 (Python) — requests.exceptions.SSLError wrapping urllib3 MaxRetryError
含义
requests 向 urllib3 请求连接,TLS 握手时证书验证失败,于是 urllib3 放弃了(requests 默认不重试,所以在第一次尝试后就放弃)。有用的信息在括号里:这里 OpenSSL 在 requests 使用的 CA 证书包中找不到颁发者,而这个证书包默认是 certifi 提供的 Mozilla 根证书副本,不是操作系统的证书存储。
原因
requests 使用 certifi 验证,而不是系统证书存储
常见公司 CA 或私有 CA,无论是用 update-ca-certificates 安装的,还是放在 Windows 证书存储或 macOS 钥匙串中的,都会被忽略:requests.certs.where() 指向的是 certifi 的 cacert.pem。复现结果显示的正是这种情况。
执行 TLS 检查的代理对流量重新签名
常见对公共 API 的请求只在公司网络中失败,因为代理的根证书不在 certifi 中。
服务器没有发送中间证书
偶见如果 API 服务器只发送叶子证书,OpenSSL 无论用哪个证书包都无法连到根证书;浏览器会获取中间证书,从而掩盖了这个问题。
REQUESTS_CA_BUNDLE 或 CURL_CA_BUNDLE 指向了不完整的证书包
偶见只包含公司根证书的证书包会让所有公共主机验证失败(反之亦然);这些变量会替换 certifi,而不是在其基础上追加。
修复方法
- 1.
通过 verify= 传入正确的证书包
verify 接受 CA 证书包文件的路径,或按哈希命名的 CA 目录的路径;主机名和日期检查仍然保持开启。
python import requests # A CA file (or a directory prepared with c_rehash) for this call requests.get("https://api.example.com/", verify="/etc/ssl/certs/ca-certificates.crt") - 2.
为进程设置 REQUESTS_CA_BUNDLE
把它指向系统证书包(先把你的 CA 安装进去),该进程中的每次 requests 调用就会与 curl 和操作系统工具信任相同的根证书。
bash # For every requests call in the process (also honoured: CURL_CA_BUNDLE) export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt # Debian/Ubuntu system bundle python3 app.py - 3.
使用操作系统的证书存储
在启动时调用 truststore.inject_into_ssl(),requests 就会通过操作系统进行验证,这样 IT 部门部署的根证书无需维护证书包即可生效。
python # pip install truststore==0.10.5 — requests then verifies with the OS trust store import truststore truststore.inject_into_ssl() import requests requests.get("https://api.example.com/") - 4.
只配置一次 Session 会话
对于应用程序,把 Session.verify 设置为同时包含公共根证书和公司根证书的证书包,并复用该会话。
python import requests session = requests.Session() session.verify = "/etc/ssl/private-ca/corp-ca-bundle.pem" # public roots + company root session.get("https://api.example.com/")