1

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

在此处输入图像描述

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

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 个质询。在 IIS 中启用了 keep-alive 标头。

IIS 版本:10.0.18362.1(Windows 10)

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

4

1 回答 1

1

首先,我尝试保存 Authorization 标头以便在每个新请求中重新使用它。

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;
    }
}

但它没有用。尽管有授权标头,但每次都有 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 的 Failed Requests Tracing 在接收到重用的身份验证标头时报告以下内容:

财产 价值
模块名称 Windows身份验证模块
通知 AUTHENTICATE_REQUEST
Http状态 401
HttpReason 未经授权
HttpSubStatus 1
错误代码 提供给函数的令牌无效 (0x80090308)

我进行了一项研究,我可以说如果没有活跃的联系,这是不可能的。

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

根据这个这个答案 NTLM 对连接进行身份验证,因此您需要保持连接打开。

NTLM over http 正在使用HTTP 持久连接或 http keep-alive。

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

如果使用相同的身份验证连接,则无需再发送身份验证标头。

这也是 NTLM 不能与某些不支持保持连接的代理服务器一起使用的原因。


更新:

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

首先:您必须在 IIS 上启用keep-alive

第二:您必须将authPersistSingleRequest标志设置为false。将此标志设置为 True 指定仅对连接上的单个请求进行身份验证。IIS 在每个请求结束时重置身份验证,并在会话的下一个请求上强制重新进行身份验证。默认值为假。 authPersistSingleRequest

第三:您可以强制HttpClient发送保持活动的标头:

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

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

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

框架 HttpClient的实现
.Net 框架 WebRequest的包装器
.Net 核心 < 2.1 本机处理程序(WinHttpHandler / CurlHandler)
.Net 核心 >= 2.1 套接字HttpHandler

我在 .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();
}

关键点是使用启用了UnsafeAuthenticatedConnectionSharing选项的WebRequestHandler并使用凭证缓存。

如果此属性设置为 true,则用于检索响应的连接在执行身份验证后保持打开状态。在这种情况下,将此属性设置为 true 的其他请求可以使用连接而无需重新验证。换句话说,如果一个连接已经被用户 A 认证,用户 B 可以重用 A 的连接;用户 B 的请求是根据用户 A 的凭据完成的。

警告 因为应用程序可能在未经身份验证的情况下使用连接,所以在将此属性设置为 true 时,您需要确保系统中没有管理漏洞。如果您的应用程序为多个用户发送请求(模拟多个用户帐户)并依赖身份验证来保护资源,请不要将此属性设置为 true,除非您使用如下所述的连接组。

非常感谢这篇文章的解决方案。

线鲨截图

于 2021-12-23T15:24:50.127 回答