.NET 6中HttpClient传递NTLM凭证及动态配置延迟排查
NTLM凭证动态更新延迟问题分析
我在API项目里用appsettings.json存储服务账户信息,该配置可通过自建配置中心动态修改。原代码每次请求都会新建HttpClient并传递NTLM凭证,结果引发了套接字耗尽问题。
之后我改成通过依赖注入注入单例HttpClient,在Program.cs中配置SocketsHttpHandler的NTLM凭证,但现在动态修改appsettings.json里的账户信息后,Web应用需要1-2分钟才会生效,而原代码是实时生效的。想知道这个延迟问题的原因是什么?
原代码片段
public DomainManagementClient(IConfiguration configuration) { _configuration = configuration; } public List<ManagedDomains> GetDomainDropdownList() { List<ManagedDomains> OnlineDCs = new List<ManagedDomains>(); var user = _configuration["ServiceAccount:ServiceAccountLogonName"]; var pwd = _configuration["ServiceAccount:ServiceAccountPassword"]; var uri = new Uri("http://test.local/api/DomainManagement/GetAllAvailableDomains"); var credentialsCache = new CredentialCache { { uri, "NTLM", new NetworkCredential(user, pwd) } }; var handler = new HttpClientHandler() { Credentials = credentialsCache, PreAuthenticate = true }; var httpClient = new HttpClient(handler); HttpResponseMessage response = httpClient.GetAsync(uri).Result; if (response.IsSuccessStatusCode) { string data = response.Content.ReadAsStringAsync().Result; OnlineDCs = JsonConvert.DeserializeObject<List<ManagedDomains>>(data); } return OnlineDCs; }
更新后的Program.cs代码
builder.Configuration.AddJsonFile($"{Directory.GetParent(Directory.GetCurrentDirectory())}/API/appsettings.json", optional: true, reloadOnChange: true); builder.Services.AddHttpClient<IDomainManagementClient, DomainManagementClient>(client => { client.BaseAddress = new Uri("http://test.local/api/api/DomainManagement/GetAllAvailableDomains"); }).ConfigurePrimaryHttpMessageHandler(() => { var user = builder.Configuration["ServiceAccount:ServiceAccountLogonName"]; var pwd = builder.Configuration["ServiceAccount:ServiceAccountPassword"]; var credentials = new NetworkCredential(user, pwd); return new SocketsHttpHandler { Credentials = credentials, PreAuthenticate = true }; });
更新后的DomainManagementClient.cs代码
public class DomainManagementClient : IDomainManagementClient { private readonly HttpClient _client; public DomainManagementClient(HttpClient client) { _client = client; } public List<ManagedDomains> GetDomainDropdownList() { List<ManagedDomains> OnlineDCs = new List<ManagedDomains>(); var uri = new Uri("http://test.local/api/api/DomainManagement/GetAllAvailableDomains"); HttpResponseMessage response = _client.GetAsync(uri).Result; if (response.IsSuccessStatusCode) { string data = response.Content.ReadAsStringAsync().Result; OnlineDCs = JsonConvert.DeserializeObject<List<ManagedDomains>>(data); } return OnlineDCs; } }
延迟原因
- HttpMessageHandler缓存策略:ASP.NET Core的
HttpClientFactory默认会缓存HttpMessageHandler实例,缓存周期为2分钟。你在ConfigurePrimaryHttpMessageHandler中初始化SocketsHttpHandler时,直接读取了程序启动时的配置值,后续即使配置更新,缓存的Handler实例仍会使用旧凭证,直到缓存过期才会创建新实例读取新配置,这就是延迟1-2分钟的核心原因。 - 原代码实时生效逻辑:原代码每次请求都会新建
HttpClientHandler和HttpClient,每次都会从IConfiguration读取最新的配置值,所以修改配置后能立即生效,但频繁创建HttpClient会导致套接字无法及时释放,最终引发耗尽问题。
解决方案
要兼顾配置实时更新和避免套接字耗尽,可以通过自定义DelegatingHandler+IOptionsMonitor实现:
- 定义配置类绑定服务账户信息,用
IOptionsMonitor监听配置变化 - 创建自定义
DelegatingHandler,在每次请求时获取最新凭证并附加到请求中 - 注册Handler到HttpClientFactory,确保复用Handler的同时动态更新凭证
示例代码
配置类与自定义Handler
// 绑定配置的实体类 public class ServiceAccountSettings { public string ServiceAccountLogonName { get; set; } public string ServiceAccountPassword { get; set; } } // 自定义DelegatingHandler,动态获取最新凭证 public class NtlmCredentialHandler : DelegatingHandler { private readonly IOptionsMonitor<ServiceAccountSettings> _settingsMonitor; public NtlmCredentialHandler(IOptionsMonitor<ServiceAccountSettings> settingsMonitor) { _settingsMonitor = settingsMonitor; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var currentSettings = _settingsMonitor.CurrentValue; var credentialCache = new CredentialCache { { request.RequestUri, "NTLM", new NetworkCredential(currentSettings.ServiceAccountLogonName, currentSettings.ServiceAccountPassword) } }; // 为当前请求设置凭证 var handler = new HttpClientHandler { Credentials = credentialCache, PreAuthenticate = true }; request.Properties[HttpClientHandler.SettingsKey] = handler; return await base.SendAsync(request, cancellationToken); } }
Program.cs注册配置与HttpClient
// 绑定配置节点 builder.Services.Configure<ServiceAccountSettings>(builder.Configuration.GetSection("ServiceAccount")); // 注册自定义Handler builder.Services.AddTransient<NtlmCredentialHandler>(); builder.Services.AddHttpClient<IDomainManagementClient, DomainManagementClient>(client => { // 设置正确的BaseAddress,避免重复拼接URL client.BaseAddress = new Uri("http://test.local/api/"); }) // 添加自定义Handler .AddHttpMessageHandler<NtlmCredentialHandler>();
更新DomainManagementClient
public class DomainManagementClient : IDomainManagementClient { private readonly HttpClient _client; public DomainManagementClient(HttpClient client) { _client = client; } public List<ManagedDomains> GetDomainDropdownList() { List<ManagedDomains> OnlineDCs = new List<ManagedDomains>(); // 利用BaseAddress简化URL var response = _client.GetAsync("DomainManagement/GetAllAvailableDomains").Result; if (response.IsSuccessStatusCode) { string data = response.Content.ReadAsStringAsync().Result; OnlineDCs = JsonConvert.DeserializeObject<List<ManagedDomains>>(data); } return OnlineDCs; } }
内容的提问来源于stack exchange,提问作者muttBunch
相关产品推荐
相关产品推荐

