You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

延迟原因

  1. HttpMessageHandler缓存策略:ASP.NET Core的HttpClientFactory默认会缓存HttpMessageHandler实例,缓存周期为2分钟。你在ConfigurePrimaryHttpMessageHandler中初始化SocketsHttpHandler时,直接读取了程序启动时的配置值,后续即使配置更新,缓存的Handler实例仍会使用旧凭证,直到缓存过期才会创建新实例读取新配置,这就是延迟1-2分钟的核心原因。
  2. 原代码实时生效逻辑:原代码每次请求都会新建HttpClientHandler和HttpClient,每次都会从IConfiguration读取最新的配置值,所以修改配置后能立即生效,但频繁创建HttpClient会导致套接字无法及时释放,最终引发耗尽问题。

解决方案

要兼顾配置实时更新和避免套接字耗尽,可以通过自定义DelegatingHandler+IOptionsMonitor实现:

  1. 定义配置类绑定服务账户信息,用IOptionsMonitor监听配置变化
  2. 创建自定义DelegatingHandler,在每次请求时获取最新凭证并附加到请求中
  3. 注册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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 02:17:16