WedX - журнал о программировании и компьютерных науках

Обновить токен с помощью статического HttpClient

Использование VS 2017 .Net 4.5.2

У меня следующий класс

public static class MyHttpClient
{
    //fields
    private static Lazy<Task<HttpClient>> _Client = new Lazy<Task<HttpClient>>(async () =>
    {
        var client = new HttpClient();
        await InitClient(client).ConfigureAwait(false);
        return client;
    });

    //properties
    public static Task<HttpClient> ClientTask => _Client.Value;

    //methods
    private static async Task InitClient(HttpClient client)
    {
        //resey headers
        client.DefaultRequestHeaders.Clear();
        //Set base URL, NOT thread safe, which is why this method is only accessed via lazy initialization
        client.BaseAddress = new Uri(ConfigurationManager.AppSettings["baseAddress"]);//TODO: get from web.config? File? DB?
        //create new request to obtain auth token
        var request = new HttpRequestMessage(HttpMethod.Post, "/ouath2/token"); //TODO: get from web.config? File? DB? prob consts
        //Encode secret and ID 
        var byteArray = new UTF8Encoding().GetBytes($"{ConfigurationManager.AppSettings["ClientId"]}:{ConfigurationManager.AppSettings["ClientSecret"]}");
        //Form data
        var formData = new List<KeyValuePair<string, string>>();
        formData.Add(new KeyValuePair<string, string>("grant_type", "refresh_token"));
        formData.Add(new KeyValuePair<string, string>("refresh_token", ConfigurationManager.AppSettings["RefreshToken"]));
        //set content and headers
        request.Content = new FormUrlEncodedContent(formData);
        request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
        //make request
        var result = await HttpPost(request, client).ConfigureAwait(false);
        //set bearer token
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", (string)result.access_token);

        //TODO: error handle
    }

    private static async Task<dynamic> HttpPost(HttpRequestMessage formData, HttpClient client)
    {
        using (var response = await client.SendAsync(formData).ConfigureAwait(false))
        {
            response.EnsureSuccessStatusCode();//TODO: handle this

            return await response.Content.ReadAsAsync<dynamic>().ConfigureAwait(false);
        }

    }
}

Все еще в процессе, но я наткнулся на загвоздку.

Это отлично работает, если токен нужно получить только один раз в жизни приложения, однако API, о котором я говорю, использует короткоживущие токены-носители (15 минут).

Поскольку я использую HttpClient как статический объект для повторного использования, я не могу изменить заголовки запроса по умолчанию, поскольку они не являются потокобезопасными. Но я должен запрашивать токен на предъявителя каждые 15 минут.

Как мне получить новый токен-носитель и установить заголовок по умолчанию в этом конкретном сценарии?


  • Обычно токен обновления живет дольше, чем токены доступа. Если это так, распространенным сценарием является запрос нового токена после получения HTTP403. С http-клиентом это легко реализовать в обработчике. Так; у вас есть актуальный токен обновления? и если так; это длится достаточно долго? 15.03.2018
  • токен обновления является долгоживущим, используйте его для получения нового токена на предъявителя, токен на предъявителя недолговечен (15 минут). ставлю обновление вместо носителя, опечатка исправлена 15.03.2018

Ответы:


1

update: добавлен SemaphoreSlim для «блокировки» транзакции обновления

отказ от ответственности: не тестировался, возможно, потребуется дополнительная настройка

примечание 1: семафор должен находиться в блоке try / catch / finalaly, чтобы гарантировать освобождение в случае возникновения ошибки.

примечание 2: эта версия помещает в очередь вызовы токена обновления, что значительно снижает производительность при высокой нагрузке. Чтобы исправить это; используйте индикатор bool, чтобы проверить, произошло ли обновление. Например, это может быть статический логический объект.

Цель состоит в том, чтобы использовать токен обновления только в случае необходимости. Фиксированный интервал вам не поможет, потому что однажды он может измениться. Правильный способ справиться с этим - повторить попытку при возникновении ошибки 403.

Вы можете использовать HttpClientHandler для работы с вашим HttpClient.

Переопределите SendAsync, чтобы обработать и повторить попытку 403.

Для этого вам понадобится этот https://msdn.microsoft.com/en-us/library/hh193664(v=vs.118).aspx:

С макушки моей (полу) головы это должно быть примерно так:

HttpMessageHandler

public class MyHttpMessageHandler : HttpMessageHandler
{
    private static SemaphoreSlim sem = new SemaphoreSlim(1);

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

    //test for 403 and actual bearer token in initial request
    if (response.StatusCode == HttpStatusCode.Unauthorized &&
        request.Headers.Where(c => c.Key == "Authorization")
                .Select(c => c.Value)
                .Any(c => c.Any(p => p.StartsWith("Bearer"))))
        {

            //going to request refresh token: enter or start wait
            await sem.WaitAsync();

            //some typical stuff
            var pairs = new List<KeyValuePair<string, string>>
            {
                new KeyValuePair<string, string>("grant_type", "refresh_token"),
                new KeyValuePair<string, string>("refresh_token", yourRefreshToken),
                new KeyValuePair<string, string>("client_id", yourApplicationId),
            };

            //retry do to token request
            using ( var refreshResponse = await base.SendAsync(
                new HttpRequestMessage(HttpMethod.Post, 
                   new Uri(new Uri(Host), "Token")) 
                   { 
                      Content = new FormUrlEncodedContent(pairs) 
                   }, cancellationToken))
            {
                var rawResponse = await refreshResponse.Content.ReadAsStringAsync();
                var x = JsonConvert.DeserializeObject<RefreshToken>(rawResponse);

                //new tokens here!
                //x.access_token;
                //x.refresh_token;

                //to be sure
                request.Headers.Remove("Authorization");
                request.Headers.Add("Authorization", "Bearer " + x.access_token);

                //headers are set, so release:
                sem.Release();  

                //retry actual request with new tokens
                response = await base.SendAsync(request, cancellationToken);

            }
        }

        return response;
    }
}
}

пример отправки с помощью SendAsync (также может быть GetAsync) и т. д.

public async Task<int> RegisterAsync(Model model)
{
    var response = await YourHttpClient
       .SendAsync(new HttpRequestMessage(HttpMethod.Post, new Uri(BaseUri, "api/Foo/Faa"))
    {  
        Content = new StringContent(
           JsonConvert.SerializeObject(model),
           Encoding.UTF8, "application/json")
    });

    var result = await response.Content.ReadAsStringAsync();
    return 0;
}
15.03.2018
  • Я использую методы postasync и getasync для httpclient. Woudl имеет смысл вместо этого использовать sendasync и использовать вместо этого requestmessage? устанавливает потокобезопасность заголовков сообщений запроса? 15.03.2018
  • Для ваших HttpClient: PostAsync, GetAsync и т. Д. Это просто методы фасада / оболочки для SendAsync, поэтому GetAsync/PostAsync подойдут. RequestMessage является потокобезопасным, поскольку в некотором смысле локален. Я немного расширю пример 16.03.2018
  • просмотр в первый раз должен работать, проходит, не проходит аутентификация, идет и получает новый токен и пытается снова, успешно и возвращается. Через 1 минуту выполняется новый запрос в соответствии с вашим примером, но опять же токен-носитель не существует, потому что мы изменили предъявителя только по индивидуальному запросу? или я неправильно это читаю? 16.03.2018
  • если все пойдет хорошо, на 403 вы получите новый токен доступа и новый токен обновления в строке new tokens here!. Вы должны хранить их и использовать в последующих запросах, токенах доступа для общих запросов, токенах обновления для запроса токенов обновления. 16.03.2018
  • Вы должны хранить их и использовать в последующих запросах, вот моя точка зрения, как мне это сделать, поскольку запрос - это новый запрос в моем методе вызова, и я не могу использовать заголовки по умолчанию в соответствии с вопросом? 16.03.2018
  • Я не уверен, понимаю ли я вас, но вы используете ConfigurationManager.AppSettings [RefreshToken] `для токена обновления, я предполагаю, что вы также можете записать это значение. Вы можете сделать то же самое для токена доступа (хотя оба они должны храниться в секрете). Вы также можете сохранить его в памяти или в своем объекте MyHttpClient. Если ваш вопрос о том, где применять заголовки; обычно в вызывающей функции, потому что, возможно, не для всех вызовов API требуется эта авторизация. Вы также можете переопределить SendAsync для автоматического применения этих заголовков, но вам нужно будет откуда-то получить токен. 16.03.2018
  • Позвольте нам продолжить это обсуждение в чате. 16.03.2018
  • @lemunk: Я выложил обновление, но не тестировал его, так как немного тороплюсь. Я проверю его позже, но он должен дать вам некоторые указания. 16.03.2018
  • Проблема заключается в том, что после получения ответа вы не можете повторно отправить тот же запрос. поэтому в самой последней части, где мы эффективно повторяем попытку, мы получаем вместо этого внутреннее значение 500 в качестве ответа из-за попытки повторной отправки 22.03.2018
  • Хм .. это странно. Вы уверены, что указали новый токен доступа в заголовке? 22.03.2018
  • да, потому что мы получаем ответ для выполнения проверок, поэтому я создал некоторый метод расширения для клонирования запроса. просто настройка и тестирование 22.03.2018
  • Да, уловка состоит в том, чтобы повторно использовать первоначальный запрос (или использовать точную копию) с новыми заголовками. Хотя это кажется мне странным, так как я использовал это в нескольких сценариях 22.03.2018
  • ну, у меня он работает, хотя не полностью протестирован (на этом нет метода TDD), так что я им доволен. Спасибо за помощь 22.03.2018
  • Приятно слышать, что ты нашел способ. И помните, спасибо выражается голосованием ;-) XD 23.03.2018
  • получил тебя прикрытый чувак;) 23.03.2018
  • @Stefan, что касается вашей второй заметки, используйте индикатор bool, чтобы проверить, произошло ли обновление. Например, это может быть static bool - я не уверен, где должна производиться проверка. Вы можете уточнить? 04.02.2019
  • Разве вы не должны сделать свой SemaphoreSlim статическим? 17.07.2019
  • @IRONicMAN: Думаю, это действительно имеет смысл :-) 17.07.2019
  • Новые материалы

    Объяснение документов 02: BERT
    BERT представил двухступенчатую структуру обучения: предварительное обучение и тонкая настройка. Во время предварительного обучения модель обучается на неразмеченных данных с помощью..

    Как проанализировать работу вашего классификатора?
    Не всегда просто знать, какие показатели использовать С развитием глубокого обучения все больше и больше людей учатся обучать свой первый классификатор. Но как только вы закончите..

    Работа с цепями Маркова, часть 4 (Машинное обучение)
    Нелинейные цепи Маркова с агрегатором и их приложения (arXiv) Автор : Бар Лайт Аннотация: Изучаются свойства подкласса случайных процессов, называемых дискретными нелинейными цепями Маркова..

    Crazy Laravel Livewire упростил мне создание электронной коммерции (панель администратора и API) [Часть 3]
    Как вы сегодня, ребята? В этой части мы создадим CRUD для данных о продукте. Думаю, в этой части я не буду слишком много делиться теорией, но чаще буду делиться своим кодом. Потому что..

    Использование машинного обучения и Python для классификации 1000 сезонов новичков MLB Hitter
    Чему может научиться машина, глядя на сезоны новичков 1000 игроков MLB? Это то, что исследует это приложение. В этом процессе мы будем использовать неконтролируемое обучение, чтобы..

    Учебные заметки: создание моего первого пакета Node.js
    Это мои обучающие заметки, когда я научился создавать свой самый первый пакет Node.js, распространяемый через npm. Оглавление Глоссарий I. Новый пакет 1.1 советы по инициализации..

    Забудьте о Matplotlib: улучшите визуализацию данных с помощью умопомрачительных функций Seaborn!
    Примечание. Эта запись в блоге предполагает базовое знакомство с Python и концепциями анализа данных. Привет, энтузиасты данных! Добро пожаловать в мой блог, где я расскажу о невероятных..


    Для любых предложений по сайту: [email protected]