客户端和服务器协商 HTTPS 连接时,首先需要建立 TLS 连接。 作为 TLS 握手的一部分,客户端会在其中一个 TLS 扩展中发送要连接到的服务器域名。 在同一台计算机上托管多个(虚拟)服务器时,TLS 协议的此功能允许客户端区分它们连接到哪些服务器并相应地配置 TLS 设置,例如服务器证书。
使用 HttpClient
HTTP 请求时,实现会根据客户端连接到的 URL 自动选择服务器名称指示 (SNI) 扩展的值。 对于需要更多手动控制扩展的方案,可以使用以下方法之一。
主机标头
主机 HTTP 标头执行与 TLS 中的 SNI 扩展类似的功能。 它允许目标服务器区分单个 IP 地址上多个主机名的请求。
HttpClient
会使用请求 URI 自动填充主机标头。 但是,还可以手动设置其值,并且 HttpClient
还将在 SNI 扩展中使用新值。 可以使用任一 HttpRequestMessage.Headers.Host
或 HttpClient.DefaultRequestHeaders.Host
实现此效果。
using HttpClient client = new();
client.DefaultRequestHeaders.Host = "www.microsoft.com";
using var response = await client.GetAsync("https://127.0.0.1:5001/");
System.Console.WriteLine(response);
注释
此方法不允许在连接到带有主机名的 URL 时完全不发送 SNI。 如果标头设置为空字符串, HttpClient
请改用 URL 中的主机名。
注释
自定义主机标头会影响服务器证书验证。 默认情况下,客户端需要服务器证书与 Host 标头中的主机名匹配。
通过 ConnectCallback 手动 SslStream 身份验证
一个更复杂但也更强大的选项是使用SocketsHttpHandler.ConnectCallback
。 自 .NET 7 起,可以返回经过 SslStream
身份验证的对象,从而自定义 TLS 连接的建立方式。 在回调中,任意 SslClientAuthenticationOptions
选项可用于执行客户端身份验证。
var handler = new SocketsHttpHandler
{
ConnectCallback = async (context, cancellationToken) =>
{
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
try
{
await socket.ConnectAsync(context.DnsEndPoint, cancellationToken);
var sslStream = new SslStream(new NetworkStream(socket, ownsSocket: true));
// When using HTTP/2, you must also keep in mind to set options like ApplicationProtocols
await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions
{
TargetHost = context.DnsEndPoint.Host,
}, cancellationToken);
return sslStream;
}
catch
{
socket.Dispose();
throw;
}
}
};
using HttpClient client = new(handler);
using var response = await client.GetAsync("https://www.microsoft.com");
System.Console.WriteLine(response);