为 HttpClientFactory 服务中使用的 DelegatingHandler 添加动态参数

问题描述 投票:0回答:2

我正在尝试在我的应用程序中使用 IHttpClientFactory,以便有效地管理在其整个生命周期中使用的 HTTP 客户端。

这些客户端中的每一个都需要根据特定的客户端 ID 设置其承载令牌,该客户端 ID 仅在运行时已知(在任何时候我们都可以使用多个客户端 ID),因此本质上这是我必须使用的参数获取授权标头。因为这些标头经常过期(我无法控制它何时会发生),所以我想重置令牌标头并在 401 状态响应上重试调用。

我尝试使用 HTTPClientService 创建它们,它通过依赖注入接收 IHTTPClientFactory:


    internal class HttpClientService
    {
        private readonly IHttpClientFactory _httpClientFactory;

        public HttpClientService(IHttpClientFactory httpClientFactory)
        {
            _httpClientFactory = httpClientFactory;
        }

        //string should be the Namespace????
        public async Task<HttpClient> GetHttpClientAsync(string clientId)
        {

            var client = _httpClientFactory.CreateClient("BasicClient");
            //get token
            client.DefaultRequestHeaders.Authorization = await GetBearerTokenHeader(clientId);

            return client;
        }
    }

因为令牌经常过期(我无法控制它何时会发生),我想重置令牌标头并重试 401 状态响应的调用,所以我使用自定义 HttpMessageHandler 来执行此操作:

services.AddHttpClient("BasicClient").AddPolicyHandler(GetRetryPolicy()).AddHttpMessageHandler<TokenFreshnessHandler>();

TokenFreshnessHandler类:

public class TokenFreshnessHandler : DelegatingHandler
    {
        public TokenFreshnessHandler()
        {
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var response = await base.SendAsync(request, cancellationToken);

            if (response.StatusCode != HttpStatusCode.Unauthorized)
            {
                //REFRESH TOKEN AND SEND AGAIN!!!
                request.Headers.Authorization = await GetBearerTokenHeader(/** CLIENT ID  CANNOT BE RETRIEVED**/);
                response = await base.SendAsync(request, cancellationToken);
            }
            return response;
        }
    }

但不幸的是,当我有机会重置令牌标头时,我无法知道原始的 ClientID 参数

是否有一种有效的方法可以让 HTTPRequestMessage 访问我的 ClientID?

我能否以某种方式使用我的 HTTPClient 将一些值传递到 HTTPRequest 消息的 Options 属性中?

c# httprequest httpclient delegatinghandler ihttpclientfactory
2个回答
0
投票

好吧,当然有一些方法可以做到这一点,而无需更改请求。

但这完全取决于您将如何使用该

HttpClientService
以及它如何在
DI
中注册。

因此,例如,您可以在

DI
中有一个“会话”对象,它被注册为单例,但该值被视为本地值,因此您可以使用工厂来获取它的最新版本。

public class ClientSession
{ 
    private AsyncLocal<string> clientId { get; set; }
    public string? ClientId 
    { 
        get => this.clientId?.Value; 
        set => this.clientId?.Value = value; 
    }
}

public class ClientSessionFactory : IClientSessionFactory
{
    private readonly IServiceProvider serviceProvider;

    public ClientSessionFactory(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider;
    }
    
    public ClientSession Create()
    {
        return serviceProvider.GetRequiredService<ClientSession>();
    }
}

所以,这个工厂可以注入到里面

HttpHandler

private readonly IClientSessionFactory ClientSessionFactory;

public TokenFreshnessHandler(IClientSessionFactory ClientSessionFactory)
{
    this.ClientSessionFactory = ClientSessionFactory;
}

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
    var clientSession = ClientSessionFactory.Create();
}

您可以在创建客户之前设置

client id

var clientSession = ClientSessionFactory.Create();
clientSession.ClientId = "xpto";
var httpClient = httpClientService.GetHttpClientAsync();

就是这个主意。

此外,您可以调整它以像这样使用:

using (var clientSession = ClientSessionFactory.Create())
{
    clientSession.ClientId = "xpto";
    var httpClient = httpClientService.GetHttpClientAsync();
}

但是你必须实施一些处置机制。


0
投票

当将数据从一个

DelegatingHandler
传递到另一个关于
HttpRequestMessage
的数据时,请使用
Options
属性。

但是当将数据从一个 Polly 策略传递到另一个策略时(通过使用

PolicyWrap
HttpClient
上的链式策略包装),那么您应该使用 Polly 的
Context
功能。这可以通过方便的扩展方法来完成。

在您的主机设置代码中:

services.AddHttpClient<IHttpService, HttpService>()
        .AddPolicyHandler(_ => Policy<HttpResponseMessage>.HandleResult(r => r.StatusCode == HttpStatusCode.Unauthorized)
                                                          .RetryAsync(1))
        .AddHttpMessageHandler<AuthorizationMessageHandler>();

包装 HTTP 客户端的 HTTP 服务

public class HttpService : IHttpService
{
    private readonly HttpClient _httpClient;

    public HttpService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<ApiResponse?> GetResource()
    {
        var request = new HttpRequestMessage(HttpMethod.Get, "https://myurl.com");

        return await Get<ApiResponse>(request);
    }

    private async Task<TResponse?> Get<TResponse>(HttpRequestMessage request)
        where TResponse : class
    {
        request.SetPolicyExecutionContext(new Context { ["clientId"] = "myClientId" });

        var responseMessage = await _httpClient.SendAsync(request);
        var response = await JsonSerializer.DeserializeAsync<TResponse>(await responseMessage.Content.ReadAsStreamAsync());

        return response;
    }
}

public interface IHttpService
{
    Task<ApiResponse?> GetResource();
}

public class ApiResponse { }

您的

AuthorizationMessageHandler
确保始终在请求消息上设置令牌:

public class AuthorizationMessageHandler : DelegatingHandler
{
    private readonly ITokenAcquisitionService _tokenAcquisitionService;

    public AuthorizationMessageHandler(ITokenAcquisitionService tokenAcquisitionService)
    {
        _tokenAcquisitionService = tokenAcquisitionService;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var context = request.GetPolicyExecutionContext();
        var clientId = context?["clientId"] as string ?? throw new InvalidOperationException("No clientId found in execution context");

        var token = await _tokenAcquisitionService.GetToken(clientId);
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);

        return await base.SendAsync(request, cancellationToken);
    }
}

获取授权令牌的服务接口:

public interface ITokenAcquisitionService
{
    Task<string> GetToken(string clientId);
}
© www.soinside.com 2019 - 2024. All rights reserved.