Добавьте динамический параметр в DelegatingHandler, используемый в службе HttpClientFactory

Я пытаюсь использовать IHttpClientFactory в своем приложении, чтобы эффективно управлять HTTP-клиентами, используемыми на протяжении всего его жизненного цикла.

Каждому из этих клиентов необходимо настроить свой токен носителя в зависимости от конкретного идентификатора клиента, который известен только во время выполнения (в любой момент у нас может быть несколько используемых идентификаторов клиента), поэтому, по сути, это параметр, который я должен использовать для получения заголовок авторизации. Поскольку срок действия этих заголовков часто истекает (что-то, что я не могу контролировать, когда это произойдет), я хочу сбросить заголовок токена и повторить вызов в ответе о статусе 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.

Есть ли эффективный способ сделать мой ClientID доступным для HTTPRequestMessage?

Могу ли я каким-то образом использовать свой HTTPClient для передачи некоторых значений в свойство Options сообщения HTTPRequest?

Я хотел бы предложить добавить clientId в качестве http-заголовка, а затем получить этот заголовок в обработчике. Внутри обработчика проверьте этот заголовок clientId, добавьте авторизацию и удалите заголовок.

Bruno Warmling 28.04.2023 19:54

Эй, Бруно, спасибо за идею, я тоже об этом думал, но это немного "хаки", я надеялся, что есть лучший способ сделать это или даже другой подход.

user14101378 28.04.2023 20:08
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
113
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ну, есть несколько способов сделать это, конечно, без изменения запроса.

Но все зависит от того, как вы собираетесь использовать этот 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();
}

Но тогда вам нужно реализовать какой-то механизм утилизации.

Эй, Бруно, интересная мысль, но будет ли это проблемой, если я попытаюсь использовать 2 HTTPClient в одном и том же контексте ожидания?

user14101378 29.04.2023 13:00
Ответ принят как подходящий

При передаче данных от одного DelegatingHandler к другому в отношении HttpRequestMessage используйте свойство Options.

Но при передаче данных из одной политики Polly в другую (в оболочке с использованием PolicyWrap или связанных политик в HttpClient) вам следует использовать функцию Context Polly. Это можно сделать с помощью удобного метода расширения.

В коде настройки хоста:

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

Спасибо за ответ, это действительно то, что сработает во многих случаях, но в моей ситуации мне также нужно передать свой HTTP-клиент в качестве аргумента другим библиотекам... Я думал, что могу создать фиктивный класс, который наследует HTTPClient, а также содержит в свойстве HTTPClient, созданный IHTTPClientFactory. Таким образом, я могу переопределить SendAsync и сохранить ClientID в своем новом классе и использовать его там. Но, по сути, мне пришлось бы создать экземпляр базового конструктора нового класса с помощью «DummyHandler» и незаметно передать все запросы новому клиенту, что является неудачной схемой.

user14101378 29.04.2023 12:57

Я был бы очень признателен за любые отзывы об идее выше. Из того, что я читал о HTTPClients, я думаю, что это сработает, но с точки зрения передовой практики это похоже на взлом :)

user14101378 29.04.2023 13:00

Я считаю, что если ваш подход требует, чтобы вы передавали одного и того же клиента другим службам, значит, в вашем дизайне есть изъян. HttpClient — это просто оболочка конвейера обработчиков сообщений. Чего вы пытаетесь достичь?

silkfire 29.04.2023 22:41

К сожалению, я вынужден использовать библиотеку, которая требует от меня передачи этих http-клиентов в качестве параметров (одновременно у меня будет несколько идентификаторов ClientID, для которых может потребоваться HTTPClient). Они используются только в течение короткого промежутка времени, и впоследствии их можно удалить, поэтому IHTTPClientFactory кажется идеальным выбором, но отсутствие конфигурации немного блокирует

user14101378 30.04.2023 08:53

Другие вопросы по теме