首先,我尝试保存 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 在每个请求结束时重置身份验证,并在会话的下一个请求上强制重新进行身份验证。默认值为假。
第三:您可以强制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,除非您使用如下所述的连接组。
非常感谢这篇文章的解决方案。