重新设置单例httpclient的证书(Reconfiguring IHttpclientFactory?)

Re-set the certificate of singleton httpclient (Reconfiguring IHttpclientFactory?)

使用 C#、.NET Core 3.1

我通过 startup.cs:

添加了一个单例 httpclient
services.AddHttpClient<IClientLogic, ClientLogicA>().ConfigurePrimaryHttpMessageHandler(() =>
{
   var handler = new HttpClientHandler();
   
   var cert= GetCertFromX();

   handler.ClientCertificates.Add(cert);

   return handler;
});

但是可以说,稍后在 ClientLogicA class 中,我想更改证书,我该怎么做,更改是否会在以后使用 httpclient 单例时持续存在?

所以您要做的是修改 HttpClient 的证书,该证书由 IHttpClientFactory 生成。看起来 Microsoft 可能会在 .NET 5 中添加此类功能,但与此同时,我们现在需要想出一种方法来实现它。

此解决方案适用于命名 HttpClient 和类型化 HttpClient 对象。

所以这里的问题是创建命名或类型化 HttpClient,其中绑定到 HttpClient 的证书集合可以随时更新。问题是我们只能为 HttpClient 设置一次创建参数。之后,IHttpClientFactory 一遍又一遍地重复使用这些设置。

那么,让我们先看看如何注入我们的服务:

Named HttpClient Injection Routine

services.AddTransient<IMyService, MyService>(); 

services.AddSingleton<ICertificateService, CertificateService>();

services.AddHttpClient("MyCertBasedClient").
    ConfigurePrimaryHttpMessageHandler(sp =>
    new CertBasedHttpClientHandler(
        sp.GetRequiredService<ICertificateService>()));

Typed HttpClient Injection Routine

services.AddSingleton<ICertificateService, CertificateService>();

services.AddHttpClient<IMyService, MyService>().
    ConfigurePrimaryHttpMessageHandler(sp =>
        new CertBasedHttpClientHandler(
            sp.GetRequiredService<ICertificateService>()));

我们将 ICertificateService 作为 单例 注入,它持有我们当前的证书并允许其他服务更改它。 IMyService 在使用 Named HttpClient 时手动注入,而在使用 Typed HttpClient 时,IMyService 将被自动注入。当 IHttpClientFactory 创建我们的 HttpClient 时,它将调用 lambda 并生成一个扩展的 HttpClientHandler,它将我们的 ICertificateService 从我们的服务管道中作为构造函数参数.

下一部分是 ICertificateService 的来源。此服务使用“id”维护证书(这只是上次更新时间的时间戳)。

CertificateService.cs

public interface ICertificateService
{
    void UpdateCurrentCertificate(X509Certificate cert);
    X509Certificate GetCurrentCertificate(out long certId);
    bool HasCertificateChanged(long certId);
}

public sealed class CertificateService : ICertificateService
{
    private readonly object _certLock = new object();
    private X509Certificate _currentCert;
    private long _certId;
    private readonly Stopwatch _stopwatch = new Stopwatch();

    public CertificateService()
    {
        _stopwatch.Start();
    }

    public bool HasCertificateChanged(long certId)
    {
        lock(_certLock)
        {
            return certId != _certId;
        }
    }

    public X509Certificate GetCurrentCertificate(out long certId)
    {
        lock(_certLock)
        {
            certId = _certId;
            return _currentCert;
        }
    }

    public void UpdateCurrentCertificate(X509Certificate cert)
    {
        lock(_certLock)
        {
            _currentCert = cert;
            _certId = _stopwatch.ElapsedTicks;
        }
    }
}

最后一部分是实现自定义 HttpClientHandler 的 class。有了这个,我们可以连接到客户端发出的 all HTTP 请求。如果证书已更改,我们会在发出请求之前将其换掉。

CertBasedHttpClientHandler.cs

public sealed class CertBasedHttpClientHandler : HttpClientHandler
{
    private readonly ICertificateService _certService;
    private long _currentCertId;

    public CertBasedHttpClientHandler(ICertificateService certificateService)
    {
        _certService = certificateService;
        var cert = _certService.GetCurrentCertificate(out _currentCertId);
        if(cert != null)
        {
            ClientCertificates.Add(cert);
        }
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, 
        CancellationToken cancellationToken)
    {
        if(_certService.HasCertificateChanged(_currentCertId))
        {
            ClientCertificates.Clear();
            var cert = _certService.GetCurrentCertificate(out _currentCertId);
            if(cert != null)
            {
                ClientCertificates.Add(cert);
            }
        }
        return base.SendAsync(request, cancellationToken);
    }
}

现在我认为最大的 down-side 是如果 HttpClient 正处于另一个线程的请求中间,我们可以 运行 进入竞争条件。您可以通过使用 SemaphoreSlim 或任何其他异步线程同步模式保护 SendAsync 中的代码来缓解这种情况,但这可能会导致 bottle-neck,所以我没有费心这样做。如果你想看到添加,我会更新这个答案。