JwtBearerEvents.OnMessageReceived не вызывается для вызова первой операции - PullRequest
8 голосов
/ 10 апреля 2020

Я использую WSO2 в качестве своего провайдера идентификации (IDP). Он помещает JWT в заголовок под названием «X-JWT-Assertion».

Чтобы передать это в ядро ​​системы ASP. NET, я добавил событие OnMessageReceived. Это позволяет мне установить token на значение, указанное в заголовке.

Вот код, который мне нужно сделать (ключевая часть - последние 3 строки кода без скобок):

services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddJwtBearer(async options =>
{
    options.TokenValidationParameters = 
         await wso2Actions.JwtOperations.GetTokenValidationParameters();

    options.Events = new JwtBearerEvents()
    {
        // WSO2 sends the JWT in a different field than what is expected.
        // This allows us to feed it in.
        OnMessageReceived = context =>
        {
            context.Token = context.HttpContext.Request.Headers["X-JWT-Assertion"];
            return Task.CompletedTask;
        }
    }
};

Все это прекрасно работает за исключением самого первого звонка после запуска службы. Чтобы было понятно, каждый звонок, кроме первого, работает именно так, как я хочу. (Он вставляет токен и обновляет объект User так, как мне нужно.)

Но при первом вызове OnMessageReceived не срабатывает. И объект User в моем контроллере не настроен.

Я проверил HttpContext для этого первого вызова, и заголовок "X-JWT-Assertion" находится в списке Request.Headers (с JWT в этом). Но по какой-то причине событие OnMessageReceived не вызывается для него.

Как я могу заставить OnMessageReceived быть вызванным для первого вызова сервисной операции для моего сервиса?

ВАЖНОЕ ПРИМЕЧАНИЕ: Я понял, что проблема была async await в AddJwtBearer. (См. Мой ответ ниже.) Это то, что я действительно хотел из этого вопроса.

Однако, поскольку вознаграждение не может быть отменено, я по-прежнему буду назначать вознаграждение любому, кто может показать способ использования AddJwtBearer с async await там, где он ожидает фактического вызова HttpClient , Или покажите документацию о том, почему async await не предполагается использовать с AddJwtBearer.

Ответы [ 3 ]

6 голосов
/ 13 апреля 2020

ОБНОВЛЕНИЕ:
Лямбда - это метод Action. Ничего не возвращает. Поэтому попытка выполнить асинхронность в нем невозможна без ее запуска и забывания.

Кроме того, этот метод вызывается при первом вызове. Поэтому ответ заключается в том, чтобы заранее вызывать все, что вам нужно в этом методе, и кэшировать его. (Однако я не нашел нехакерского способа использовать элементы, добавленные зависимостями, для выполнения этого вызова.) Затем во время первого вызова будет вызываться эта лямбда-выражение. В это время вы должны извлечь нужные значения из кэша (таким образом, не сильно замедляя первый вызов).


Это то, что я наконец понял.

Лямбда для AddJwtBearer не работает с async await. Мой вызов на await wso2Actions.JwtOperations.GetTokenValidationParameters(); ждет просто отлично, , но конвейер вызова продолжается без ожидания AddJwtBearer до конца sh.

С async await порядка вызовов выглядит так:

  1. Служба запускается (и вы некоторое время ждете, чтобы все это было счастливым.)
  2. Вызов в службу.
  3. AddJwtBearer вызывается.
  4. await wso2Actions.JwtOperations.GetTokenValidationParameters(); вызывается.
  5. GetTokenValidationParameters() вызывает HttpClient с await.
  6. * * * HttpClient делает ожидаемый вызов, чтобы получить подписывающий ключ подписчика c эмитента.
  7. Пока HttpClient ожидает, остальная часть исходного вызова проходит. Пока еще не было настроено ни одного события, поэтому он продолжает работу с конвейером вызовов как обычно.
    • Здесь "пропускается" событие OnMessageReceived.
  8. HttpClient получает ответ с помощью клавиши publi c.
  9. Выполнение AddJwtBearer продолжается.
  10. Событие OnMessageReceived настроено.
  11. Второй вызов сделан службе
  12. Поскольку событие было в конечном итоге настройка, событие называется. (AddJwtBearer вызывается только при первом вызове.)

Итак, когда происходит ожидание (в этом случае он в конечном итоге выполняет вызов HttpClient для получения ключа подписи эмитента), остальные Первый звонок проходит. Поскольку еще не было настроено событие, он не знает, как вызвать обработчик.

Я изменил лямбда AddJwtBearer, чтобы он не был асин c, и он работал просто отлично.

Примечания:
Две вещи кажутся здесь странными:

  1. Я бы подумал, что AddJwtBearer будет вызываться при запуске, а не при первом вызове службы.
  2. Я бы подумал, что AddJwtBearer не будет поддерживать async лямбда-сигнатуру, если он не сможет правильно применить await.

Я не уверен, что это ошибка или нет, но я разместил его на всякий случай: https://github.com/dotnet/aspnetcore/issues/20799

0 голосов
/ 18 апреля 2020

Причина, по которой ваши первые запросы пары не могут вызвать OnMessageReceived, не в том, что вы используете делегат async void, а в порядке загрузки параметров и присоединяемых событий.

Вы присоединяете обработчики событий после await, что означает, что вы создали здесь условие гонки, что, если, скажем, какой-то запрос поступит до завершения await, к OnMessageReceived не будет никакого обработчика событий.

Чтобы это исправить, вы должны прикрепить обработчики событий до первого await. Это гарантирует, что у вас всегда есть обработчики событий, прикрепленные к OnMessageReceived.

Попробуйте этот код:

services.AddAuthentication(opt =>
    {
        // ...
    })
    .AddJwtBearer(async opt =>
    {
        var tcs = new TaskCompletionSource<object>();

        // Any code before the first await in this delegate can run
        // synchronously, so if you have events to attach for all requests
        // attach handlers before await.
        opt.Events = new JwtBearerEvents
        {
            // This method is first event in authentication pipeline
            // we have chance to wait until TokenValidationParameters
            // is loaded.
            OnMessageReceived = async context =>
            {
                // Wait until token validation parameters loaded.
                await tcs.Task;
            }
        };

        // This delegate returns if GetTokenValidationParametersAsync
        // does not complete synchronously 
        try
        {
            opt.TokenValidationParameters = await GetTokenValidationParametersAsync();
        }
        finally
        {
            tcs.TrySetResult(true);
        }

        // Any code here will be executed as continuation of
        // GetTokenValidationParametersAsync and may not 
        // be seen by first couple requests
    });
0 голосов
/ 15 апреля 2020

Вы можете использовать GetAwaiter().GetResult() для выполнения асинхронного c кода при запуске. Он заблокирует поток, но это нормально, потому что он запускается только один раз, и он запускается при запуске приложения.

Однако, если вы не хотите блокировать поток и настаивайте на использовании await, чтобы получить Параметры, вы можете использовать async await в Program.cs, чтобы получить ваши параметры и сохранить его в классе c и использовать его при запуске.

public class Program
{
    public static async Task Main(string[] args)
    {
        JwtParameter.TokenValidationParameters = await wso2Actions.JwtOperations.GetTokenValidationParameters();
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

public static class JwtParameter
{
    public static TokenValidationParameters TokenValidationParameters { get; set; }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...