Python Requests 抛出 SSLError 错误

在用python写爬虫抓取一些https站点的时候遇到过这样的错误:

requests.exceptions.SSLError: [Errno 1] _ssl.c:503: error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed

解决方案一

requests抛出了一个SSLError,解决这个问题最简单的方式是关闭校验。

response = requests.get(url, verify=False)

解决方案二

当然关闭校验会引发中间人攻击这样的安全问题,但是对于爬虫系统来说还好。不过细究一下原因,可以归结到Python 2.x 不支持 SNI。

pyOpenSSL对SNI问题加了猴子补丁,有对这个问题的解决方案。

首先安装依赖。

  • pyOpenSSL == 0.13
  • ndg-httpsclient == 0.3.2
  • pyasn1 == 0.1.6
pip install pyopenssl ndg-httpsclient pyasn1

然后在使用urllib3之前,即requests之前写以下代码。

try:
    import urllib3.contrib.pyopenssl
    urllib3.contrib.pyopenssl.inject_into_urllib3()
except ImportError:
    pass

另外requests对于SSL问题有一些额外支持来解决这个问题的。安装requests的时候加参数即可安装这些额外的包依赖了。

pip install requests[security]

关于SNI

我们知道了如何在Python解决SNI引发的SSLError错误。那SNI到底是什么意思呢?

Server Name Indication,简称为 SNI,是 TLS 的一个扩展。

在HTTP/1.1协议中比之前1.0在请求头部中多一个Host字段,从而让Apache、Nginx这些Http服务可以通过该字段标识出请求属于哪个站点。这样的功能设计也促使了虚拟空间虚拟站点这类的一台服务器多站点的服务出现。

但是对于 HTTPS 网站来说,要想发送 HTTP 数据,必须等待 SSL 握手完成,而在握手阶段服务端就必须提供网站证书。对于在同一个 IP 部署不同 HTTPS 站点,并且还使用了不同证书的情况下,服务端怎么知道该发送哪个证书?因为HTTPS依赖的SSL/TLS协议并没有同步跟进,SSL握手阶段客户端向服务器端发送的信息中未包含Host,所以服务器端也就没法返回正确的HTTPS证书了。

有了 SNI,服务端可以通过 Client Hello 中的 SNI 扩展拿到用户要访问网站的 Server Name,进而发送与之匹配的证书,顺利完成 SSL 握手。

然而对于一些低版本的浏览器不支持SNI的,就悲剧了。Python这里算是一个例子。

参考引用

https://stackoverflow.com/questions/10667960/python-requests-throwing-up-sslerror

一个关于python requests 和SSL证书的问题?

HTTPS和SNI

关于启用 HTTPS 的一些经验分享