使用 NTLM 协商时,.NET HttpClient 不会在对 IIS 的请求之间保留身份验证

.NET HttpClient do not persist authentication between reqeusts to IIS when using NTLM Negotiate

IIS 站点配置为使用默认选项的 windows 身份验证。客户端是用 C# 编写的,并使用单个 HttpClient 实例来执行请求。请求成功,但每次请求都会触发401 Challenge:

使用 Wireshark 捕获的流量。我们进行了大声测试,并注意到,使用匿名身份验证的客户端每秒执行 5000 个请求,但使用 windows 身份验证 - 800。因此,看起来 wireshark 不会影响身份验证,性能下降表明,401 挑战也会发生没有 wireshark。

Wirehshark 日志:https://drive.google.com/file/d/1vDNZMjiKPDisFLq6ZDhASQZJJKuN2cpj/view?usp=sharing

客户端代码在这里:

var httpClientHandler = new HttpClientHandler();
httpClientHandler.UseDefaultCredentials = true;
var httpClient = new HttpClient(httpClientHandler);
while (working)
{
    var response = await httpClient.GetAsync(textBoxAddress.Text + "/api/v1/cards/" + cardId);
    var content = await response.Content.ReadAsStringAsync();
}

IIS 站点设置:

如何使 HttpClient 在请求之间保持身份验证,以防止在每次请求时浪费协商握手?

UPD:客户代码:https://github.com/PFight/httpclientauthtest 重现步骤:

  1. 用简单文件创建文件夹 index.html,在 IIS 中为此文件夹创建应用程序 'testsite'。启用匿名身份验证。
  2. 运行 客户端 (https://github.com/PFight/httpclientauthtest/blob/main/TestDv5/bin/Debug/TestDv5.exe),按开始按钮 - 查看每秒请求数。按停止。
  3. 禁用匿名认证,启用windows认证。
  4. 在客户端中按下开始按钮,查看每秒的请求数。

在我的计算机上,我看到每秒大约 1000 个匿名请求,而 windows 每秒大约 180 个请求。 Wireshark 对 windows 身份验证的每个请求显示 401 挑战。 keep-alive header 在 IIS 中启用。

IIS 版本:10.0.18362.1 (windows 10)

加载到进程的 System.Net.Http.dll 版本:4.8.3752.0

首先,我尝试在每个新请求中为 re-use 保存授权 header。

using System.Net.Http.Headers;

Requester requester = new Requester();
await requester.MakeRequest("http://localhost/test.txt");
await Task.Delay(100);
await requester.MakeRequest("http://localhost/test.txt");

class Requester
{
    private readonly HttpClientHandler _httpClientHandler;
    private readonly HttpClient _httpClient;
    private AuthenticationHeaderValue _auth = null;

    public Requester()
    {
        _httpClientHandler = new HttpClientHandler();
        _httpClientHandler.UseDefaultCredentials = true;
        _httpClient = new HttpClient(_httpClientHandler);
        _httpClient.DefaultRequestHeaders.Add("User-Agent", Guid.NewGuid().ToString("D"));
    }

    public async Task<string> MakeRequest(string url)
    {
        HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get, url);
        message.Headers.Authorization = _auth;

        HttpResponseMessage resp = await _httpClient.SendAsync(message);
        _auth = resp.RequestMessage?.Headers?.Authorization;
        resp.EnsureSuccessStatusCode();
        string responseText = await resp.Content.ReadAsStringAsync();
        return responseText;
    }
}

但是没有用。尽管有授权 header.

,但每次都有 http 代码 401 要求进行身份验证

下面列出了 IIS 日志。

2021-12-23 15:07:47 ::1 GET /test.txt - 80 - ::1 c75eeab7-a0ea-4ebd-91a8-21f5cd59c10f - 401 2 5 127
2021-12-23 15:07:47 ::1 GET /test.txt - 80 MicrosoftAccount\account@domain.com ::1 c75eeab7-a0ea-4ebd-91a8-21f5cd59c10f - 200 0 0 4
2021-12-23 15:07:47 ::1 GET /test.txt - 80 - ::1 c75eeab7-a0ea-4ebd-91a8-21f5cd59c10f - 401 1 2148074248 0
2021-12-23 15:07:47 ::1 GET /test.txt - 80 MicrosoftAccount\account@domain.com ::1 c75eeab7-a0ea-4ebd-91a8-21f5cd59c10f - 200 0 0 0

IIS 的失败请求跟踪在接收 re-used 身份验证时报告以下内容 Header:

Property Value
ModuleName WindowsAuthenticationModule
Notification AUTHENTICATE_REQUEST
HttpStatus 401
HttpReason Unauthorized
HttpSubStatus 1
ErrorCode The token supplied to the function is invalid (0x80090308)

我做了一个研究,我可以说没有实时连接这是不可能的。

每次关闭连接都会有新的握手。

根据 this and this 的回答,NTLM 对连接进行身份验证,因此您需要保持连接打开。

HTTP 上的 NTLM 使用 HTTP persistent connection 或 http keep-alive。

创建一个连接,然后在 session 的其余部分保持打开状态。

如果使用相同的已验证连接,则无需再发送验证 headers。

这也是 NTLM 无法与某些不支持 keep-alive 连接的代理服务器一起工作的原因。


更新:

我用你的例子找到了关键点。

首先:您必须在您的 IISenablekeep-alive

其次:您必须将authPersistSingleRequest标志设置为false。将此标志设置为 True 指定身份验证仅针对连接上的单个请求持续存在。 IIS 在每个请求结束时重置身份验证,并在 session 的下一个请求上强制 re-authentication。默认值为假。

第三种:可以强制HttpClient发送keep-alive headers:

httpClient.DefaultRequestHeaders.Add("Connection", "keep-alive");
httpClient.DefaultRequestHeaders.Add("Keep-Alive", "600");

利用这三个关键点,我在连接生命周期内只实现了一次 NTLM 握手。

您使用哪个版本的 .NET \ .NET Framework 也很重要。 因为 HttpClient 隐藏了依赖于框架版本的不同实现。

Framework Realization of HttpClient
.Net Framework Wrapper around WebRequest
.Net Core < 2.1 Native handlers (WinHttpHandler / CurlHandler)
.Net Core >= 2.1 SocketsHttpHandler

我在 .NET 6 上试过了,效果很好,但正如我所见,它在 .Net Framework 上不起作用,所以问题来了:您使用哪个平台?

更新 2:

找到 .Net Framework 的解决方案。

CredentialCache myCache = new CredentialCache();
WebRequestHandler handler = new WebRequestHandler()
{
    UseDefaultCredentials = true,
    AllowAutoRedirect = true,
    UnsafeAuthenticatedConnectionSharing = true,
    Credentials = myCache,
};
var httpClient = new HttpClient(handler);

httpClient.DefaultRequestHeaders.Add("Connection", "keep-alive");
httpClient.DefaultRequestHeaders.Add("Keep-Alive", "600");

var from = DateTime.Now;
var countPerSecond = 0;
working = true;
while (working)
{
    var response = await httpClient.GetAsync(textBoxAddress.Text);
    var content = await response.Content.ReadAsStringAsync();
    countPerSecond++;
    if ((DateTime.Now - from).TotalSeconds >= 1)
    {
        this.labelRPS.Text = countPerSecond.ToString();
        countPerSecond = 0;
        from = DateTime.Now;
    }

    Application.DoEvents();
}

关键点是使用 WebRequestHandler with UnsafeAuthenticatedConnectionSharing 启用选项并使用凭据缓存。

If this property is set to true, the connection used to retrieve the response remains open after the authentication has been performed. In this case, other requests that have this property set to true may use the connection without re-authenticating. In other words, if a connection has been authenticated for user A, user B may reuse A's connection; user B's request is fulfilled based on the credentials of user A.

Caution Because it is possible for an application to use the connection without being authenticated, you need to be sure that there is no administrative vulnerability in your system when setting this property to true. If your application sends requests for multiple users (impersonates multiple user accounts) and relies on authentication to protect resources, do not set this property to true unless you use connection groups as described below.

非常感谢 this article 的解决方案。