异步 aiohttp 请求失败,但同步请求成功

asynchronous aiohttp requests fails, but synchronous requests succeed

当我使用异步 aiohttp 时,使用以下代码我得到 Cannot connect to host ...:443 ssl:True。当我使用同步 requests 时,它成功了。

whitehouse.gov 链接失败,但 google.com 对于异步和同步情况都成功。

出了什么问题?这是在 FreeBSD8 上使用 python 3.4.2,aiohttp 0.14.4,请求 2.5.3

import asyncio
import aiohttp
import requests

urls = [
    'http://www.whitehouse.gov/cea/', 
    'http://www.whitehouse.gov/omb', 
    'http://www.google.com']


def test_sync():
    for url in urls:
        r = requests.get(url)
        print(r.status_code)


def test_async():
    for url in urls:
        try:
            r = yield from aiohttp.request('get', url)
        except aiohttp.errors.ClientOSError as e:
            print('bad eternal link %s: %s' % (url, e))
        else:
            print(r.status)


if __name__ == '__main__':
    print('async')
    asyncio.get_event_loop().run_until_complete(test_async())
    print('sync')
    test_sync()

这个输出是:

async
bad eternal link http://www.whitehouse.gov/cea: Cannot connect to host www.whitehouse.gov:443 ssl:True
bad eternal link http://www.whitehouse.gov/omb: Cannot connect to host www.whitehouse.gov:443 ssl:True
200
sync
200
200
200

我怀疑您机器上的证书验证链已损坏。 正如@dano 提到的那样,在 Ubuntu 上一切正常。

无论如何,您可以通过创建自定义 Connector 实例来禁用 ssl 验证:

import asyncio
import aiohttp

urls = [
    'http://www.whitehouse.gov/cea/',
    'http://www.whitehouse.gov/omb',
    'http://www.google.com']


def test_async():
    connector = aiohttp.TCPConnector(verify_ssl=False)
    for url in urls:
        try:
            r = yield from aiohttp.request('get', url, connector=connector)
        except aiohttp.errors.ClientOSError as e:
            print('bad eternal link %s: %s' % (url, e))
        else:
            print(r.status)


if __name__ == '__main__':
    print('async')
    asyncio.get_event_loop().run_until_complete(test_async())

顺便说一句,requests 库附带了自己的证书包。也许我们需要为 aiohttp 做同样的事情?

UPD. 另见 https://github.com/aio-libs/aiohttp/issues/341

不要使用此答案,因为它可能等同于禁用证书检查。

正如 Le Hibou 在评论中指出的那样,ssl.Purpose.CLIENT_AUTH 用于在服务器端对客户端进行身份验证。

This value indicates that the context may be used to authenticate Web clients (therefore, it will be used to create server-side sockets).

查看 create_default_context() 的代码表明在这种情况下证书检查可能被禁用或至少是可选的。


我有同样的错误消息(在 Windows 上)并用以下方法解决了它:

import aiohttp
import ssl


client = aiohttp.ClientSession()
client.post(
    'https://some.foo/bar/',
    json={"foo": "bar"},
    ssl=ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH))

这是使用 request() to set a different SSL context than the default. The default is ssl.create_default_context()ssl 参数。

问题是 ssl.create_default_context() 的默认值是:

ssl.create_default_context(purpose=Purpose.SERVER_AUTH, cafile=None, capath=None, cadata=None)

在客户端验证服务器证书时,Purpose.SERVER_AUTH 似乎不起作用。作为客户端验证时,请改为使用 Purpose.CLIENT_AUTH

我在 CA 根证书已过期的旧 Linux 服务器上遇到了同样的问题,并且在 SSLContext 中加载 certifi CA 证书包解决了这个问题。

import aiohttp
import ssl
import certifi

ssl_context = ssl.create_default_context(cafile=certifi.where())
async with aiohttp.ClientSession() as session:
    async with session.get('https://some.foo/bar/', ssl=ssl_context) as response:
        print(await response.text())

这对我有用:

if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())