如何在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
相关产品推荐
相关产品推荐

