重新设置单例HttpClient的证书(重新配置IHttpclientFactory?)

3

使用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类的后续操作中,我想要更改证书,那么我应该如何操作才能确保这个变化会持续影响到httpclient的单例对象?


单例模式在项目中只创建一个类的实例。由于所有依赖于此单例的代码使用相同的实例,因此更改实例的某些数据将持久存在。 - Simon Tulling
嗨@SimonTulling,感谢您的回复。我测试了一下,但发现情况并非如此。使用注入的实例,我为其设置了一个新的客户端实例,并在后续调用中访问它,结果发现它没有证书,就好像从未设置过一样。 - PKCS12
你在 handler.ClientCertificates.Add(cert); 上设置了断点,每次执行这个 lambda 表达式时 cert 都是有效的吗? - Andy
@Andy 是的。问题不在于启动时添加证书,而是稍后更改为不同的证书。例如,如何在保留单例 HttpClient 的同时为每个客户端请求更改证书? - PKCS12
@Andy 我愿意考虑,但是你有什么建议? :) 另外,我刚刚找到了这个,我不认为有解决的办法:https://github.com/dotnet/extensions/issues/521 - PKCS12
显示剩余2条评论
1个回答

6
所以你要做的是修改由生成的的证书。看起来微软可能会在.NET 5中添加这种功能,但在此期间,我们需要想办法立即实现它。
该解决方案适用于Named 和Typed 对象。
问题在于创建命名或类型化的,其中绑定到的证书集可以随时更新。问题在于我们只能为设置创建参数一次。之后,会一遍又一遍地重用这些设置。
所以,让我们首先看一下我们如何注入服务:

命名的HttpClient注入例程

services.AddTransient<IMyService, MyService>(); 

services.AddSingleton<ICertificateService, CertificateService>();

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

类型化的HttpClient注入程序

services.AddSingleton<ICertificateService, CertificateService>();

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

我们将一个ICertificateService 注入为单例,用于保存我们当前的证书并允许其他服务更改它。当使用Named HttpClient时,需要手动注入IMyService,而使用Typed HttpClient时,IMyService会自动注入。当IHttpClientFactory需要创建我们的HttpClient时,它会调用lambda表达式并生成一个扩展的HttpClientHandler,该Handler需要从我们的服务管道中获取ICertificateService作为构造函数参数。
下面是ICertificateService的源代码,该服务维护带有“id”的证书(这只是上次更新时间的时间戳)。
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类。使用它,我们可以挂钩到客户端发出的所有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);
    }
}

我认为这样做的最大缺点是,如果HttpClient正在另一个线程上进行请求,我们可能会遇到竞态条件。你可以通过在SendAsync中使用SemaphoreSlim或任何其他异步线程同步模式来缓解这个问题,但这可能会导致瓶颈,所以我没有尝试这样做。如果您想看到添加这个功能,我会更新这个答案。


完全测试 - 已经可以使用了!非常感谢!!我只是想知道在_performance_上,使用_DI service.AddHttpClient() on startup_ 的_per request_的_httpClientFactory.CreateClient() 和注入的_HttpClient_之间是否有很大的性能差异。 - PKCS12
1
@PKCS12 -- 我将更新此答案并添加一些代码,以说明如何使用“Typed”HttpClient对象。我不完全确定“Named”HttpClient与“Typed”有什么区别,但最终结果将是相同的。 - Andy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接