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

Posted

技术标签:

【中文标题】.NET HttpClient 在使用 NTLM 协商时不会在对 IIS 的请求之间保持身份验证【英文标题】:.NET HttpClient do not persist authentication between reqeusts to IIS when using NTLM Negotiate 【发布时间】:2021-12-21 09:16:15 【问题描述】:

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

使用 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 重现步骤:

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

在我的计算机上,我在匿名上看到每秒约 1000 个请求,在 Windows 上看到约 180 个请求。 Wireshark 在每个 Windows 身份验证请求上显示 401 个质询。在 IIS 中启用了 keep-alive 标头。

IIS 版本:10.0.18362.1(Windows 10)

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

【问题讨论】:

您可以考虑将 HttpClient 凭据与请求一起传递。 HTTP Keep-Alive 开启了吗?你能在 Wireshark 中看到每个 HTTP 请求是否有自己的 TCP 连接,还是它们共享同一个连接? TCP 连接中断。无法配置 HttpClient 以保持连接。 @Pavel 请看我更新的答案 .net 框架的另一个更新 【参考方案1】:

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

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 要求身份验证,尽管 Authorization 标头。

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 在接收到重用的 Authentication 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 和this 的回答,NTLM 对连接进行身份验证,因此您需要保持连接打开。

NTLM over http 正在使用 HTTP persistent connection 或 http keep-alive。

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

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

这也是 NTLM 不适用于某些不支持保持连接的代理服务器的原因。


更新:

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

首先:你必须在你的 IIS 上 enablekeep-alive

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

第三:你可以强制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 启用UnsafeAuthenticatedConnectionSharing 选项并使用凭据缓存。

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

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

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

【讨论】:

感谢您的研究,您确认了我们的建议,保持 tcp 连接只是方法。答案将是确定如何为 HttpClient + IIS 启用 http keep-alive。所有文档都说,HttpClient 应该最大限度地保持连接,但正如你所看到的 - 它没有。 @Pavel ,我使用this 文章禁用了 IIS 上的 keep-alive,因为启用 keep-alive 它按预期工作:交换期间只有一次握手。我认为禁用 keep-alive 是您的 IIS 设置的要求。 启用 keep-alive 的 IIS 日志:2021-12-23 13:43:21 ::1 GET /test.txt - 80 - ::1 e047713d-3eeb-4ff6-93ac-3311d33c5d85 - 401 2 5 0 2021-12-23 13:43:21 ::1 GET /test.txt - 80 MicrosoftAccount\account@domain.com ::1 e047713d-3eeb-4ff6-93ac-3311d33c5d85 - 200 0 0 0 2021- 12-23 13:43:21 ::1 GET /test.txt - 80 MicrosoftAccount\account@domain.com ::1 e047713d-3eeb-4ff6-93ac-3311d33c5d85 - 200 0 0 0 谢谢,我会尝试创建更简单的示例。如果你没有遇到keep-alive问题,那么我可能会发现最简单的网站和我们的网站之间的区别。 更新问题,共享客户端代码。问题在带有 keep-alive 标头的简单 index.html 上重现。请在您的机器上尝试使用指定场景的github.com/PFight/httpclientauthtest/blob/main/TestDv5/bin/…。你会重现问题吗?

以上是关于.NET HttpClient 在使用 NTLM 协商时不会在对 IIS 的请求之间保持身份验证的主要内容,如果未能解决你的问题,请参考以下文章

同时使用 SSL 加密和 NTLM 身份验证的 HttpClient 失败

核心中的 NTLM 身份验证 HttpClient

HttpClient 4.1.1 在使用 NTLM 进行身份验证时返回 401,浏览器工作正常

在 .NET Core 的 Web 请求中使用 NTLM 身份验证

NTLM认证流程详解

如何将 .NET WebSocket 客户端与 NTLM 代理一起使用?