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

如何在aiohttp请求中指定SNI的server_hostname?

如何在aiohttp中指定自定义SNI的server_hostname发起请求?

网络架构

load-balancer.example.com:443 (TCP透传)
                   /\
                  /  \
                 /    \
                /      \
s1.example.com:443        s2.example.com:443
    (SSL/SNI)                  (SSL/SNI)

目标

绕过负载均衡器,直接对上游服务器s1、s2进行压力测试,同时启用证书验证(所有上游服务器持有load-balancer.example.com的有效证书)。

环境限制

  • 运行环境:Python 3.7+,GNU Linux系统
  • 所有上游服务器持有load-balancer.example.com的有效证书
  • 执行openssl s_connect s1.example.com:443 -servername load-balancer.example.com可验证上游证书有效
  • 执行curl 'https://load-balancer.example.com/' --resolve s1.example.com:443:load-balancer.example.com可成功完成证书验证并请求

当前问题

直接用aiohttp请求s1/s2时,默认会将目标主机(如s1.example.com)作为SNI的server_hostname,导致上游服务器返回默认过期证书,触发SSL验证失败错误:

Cannot connect to host s1.example.com:443 ssl:True 
[SSLCertVerificationError: (1, '[SSL: CERTIFICATE_VERIFY_FAILED] '
certificate verify failed: certificate has expired (_ssl.c:1131)')]) 

需要让每个请求在SSL握手时,强制指定load-balancer.example.com作为server_hostname。

现有代码片段

import aiohttp
import asyncio


async def main_async(host, port, uri, params=[], headers={}, sni_hostname=None):
    if sni_hostname is not None:
        print('Setting SNI server_name field ')
        # 此处需要配置aiohttp,在SSL握手前将server_hostname设为sni_hostname
    try:
        async with aiohttp.ClientSession(raise_for_status=True) as session:
            async with session.get(f'https://{host}:{port}/{uri}', params=params, headers=headers) as r:
                body = await r.read()
                print(body)
    except Exception as e:
        print(f'Exception while requesting ({e}) ')


if __name__ == "__main__":
    asyncio.run(main_async(host='s1.example.com', port=443,
                           uri='/api/some/endpoint',
                           params={'apikey': '0123456789'},
                           headers={'Host': 'load-balancer.example.com'},
                           sni_hostname='load-balancer.example.com'))

已尝试的无效方案

  • ssl.SSLContext.sni_callback:该回调在SSL握手启动并收到证书后才触发,此时server_hostname为只读属性,无法修改
  • 尝试通过SSLContext.wrap_socket设置server_hostname,但未找到在aiohttp中正确集成的方式

解决方案

aiohttp允许在请求的ssl参数中传递配置字典,直接指定server_hostname,同时保留默认的证书验证逻辑。修改后的代码如下:

import aiohttp
import asyncio
import ssl


async def main_async(host, port, uri, params=[], headers={}, sni_hostname=None):
    ssl_config = None
    if sni_hostname is not None:
        # 创建默认SSL上下文,确保证书验证正常执行
        ssl_ctx = ssl.create_default_context()
        # 传递自定义server_hostname给SSL握手
        ssl_config = {"ssl_context": ssl_ctx, "server_hostname": sni_hostname}
        print(f'Setting SNI server_name field to {sni_hostname}')
    try:
        async with aiohttp.ClientSession(raise_for_status=True) as session:
            url = f'https://{host}:{port}/{uri}'
            async with session.get(url, params=params, headers=headers, ssl=ssl_config) as r:
                body = await r.read()
                print(body)
    except Exception as e:
        print(f'Exception while requesting ({e}) ')


if __name__ == "__main__":
    asyncio.run(main_async(host='s1.example.com', port=443,
                           uri='/api/some/endpoint',
                           params={'apikey': '0123456789'},
                           headers={'Host': 'load-balancer.example.com'},
                           sni_hostname='load-balancer.example.com'))

说明

  • 使用ssl.create_default_context()保证默认的证书验证逻辑不被禁用
  • 通过ssl_config字典将自定义的server_hostname传递给SSL套接字的握手过程,上游服务器会据此返回对应域名(load-balancer.example.com)的有效证书
  • 若只需指定server_hostname且不需要自定义SSL上下文,也可简化为ssl_config = {"server_hostname": sni_hostname},aiohttp会自动使用默认上下文

内容的提问来源于stack exchange,提问作者AsyncBrain

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 14:35:18