.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 凭据与请求一起传递。 HTTPKeep-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 失败
HttpClient 4.1.1 在使用 NTLM 进行身份验证时返回 401,浏览器工作正常