JwtBearerEvents.OnMessageReceived 未调用首次操作调用

JwtBearerEvents.OnMessageReceived not Called for First Operation Invocation

我正在使用 WSO2 作为我的身份提供者 (IDP)。它将 JWT 放在一个名为 "X-JWT-Assertion" 的 header 中。

为了将其输入 ASP.NET 核心系统,我添加了一个 OnMessageReceived 事件。这允许我将 token 设置为 header.

中提供的值

这是我必须执行的代码(关键部分是 non-bracket 代码的最后 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 object。)

但是对于第一次调用,OnMessageReceived 没有命中。我的控制器中的 User object 没有设置。

我检查了第一个调用的 HttpContext,"X-JWT-Assertion" header 在 Request.Headers 列表中(其中包含 JWT)。但是,出于某种原因,OnMessageReceived 事件没有被调用。

如何让 OnMessageReceived 在我的服务第一次调用服务操作时被调用?

重要提示: 我发现问题出在 async await AddJwtBearer 中。 (请参阅下面我的回答。)这就是我真正想从这个问题中得到的。

但是,由于无法取消赏金,我仍然会将赏金奖励给任何可以展示使用 AddJwtBearerasync await 的方法的人,它正在等待实际 HttpClient 调用。或者显示为什么 async await 不应该与 AddJwtBearer.

一起使用的文档

更新:
lambda 是一种 Action 方法。它没有 return 任何东西。因此,如果不触发并忘记它,就不可能尝试在其中进行异步操作。

此外,此方法在第一次调用时被调用。所以答案是提前在这个方法中调用你需要的任何东西并缓存它。 (但是,我还没有想出一种使用依赖项注入项进行此调用的非黑客方法。)然后在第一次调用期间,将调用此 lambda。那时你应该从缓存中提取你需要的值(这样不会减慢第一次调用的速度)。


这是我终于想通的。

AddJwtBearer 的 lambda 不适用于 async await。我对 await wso2Actions.JwtOperations.GetTokenValidationParameters(); 的调用等待得很好,但是调用管道继续进行而不等待 AddJwtBearer 完成。

使用 async await 调用顺序如下:

  1. 服务启动(您稍等片刻,一切顺利。)
  2. 调用服务。
  3. AddJwtBearer 被调用。
  4. await wso2Actions.JwtOperations.GetTokenValidationParameters(); 被调用。
  5. GetTokenValidationParameters()await 调用 HttpClient
  6. HttpClient 执行等待调用以获取发行者的 public 签名密钥。
  7. HttpClient 等待期间,原始呼叫的其余部分将继续进行。还没有设置任何事件,所以它只是像往常一样继续调用管道。
    • 这是 "appears to skip" OnMessageReceived 事件的地方。
  8. HttpClient 使用 public 键获得响应。
  9. AddJwtBearer 的执行继续。
  10. OnMessageReceived 事件已设置。
  11. 第二次调用该服务
  12. 因为最终设置了事件,所以调用了事件。 (AddJwtBearer 仅在第一次调用时被调用。)

因此,当等待发生时(在这种情况下,它最终会调用 HttpClient 以获取颁发者签名密钥),第一个调用的其余部分将通过。因为还没有事件设置,它不知道调用处理程序。

我将 AddJwtBearer 的 lambda 更改为非异步并且它工作得很好。

备注:
这里有两件事看起来很奇怪:

  1. 我本以为 AddJwtBearer 会在启动时被调用,而不是在第一次调用服务时被调用。
  2. 如果 AddJwtBearer 无法正确应用 await,我会认为它不会支持 async lambda 签名。

我不确定这是否是一个错误,但我将其发布为以防万一:https://github.com/dotnet/aspnetcore/issues/20799

您可以使用GetAwaiter().GetResult()在启动时执行异步代码。它会阻塞线程,但没关系,因为它只 运行 一次并且它在应用程序的启动中。

不过如果你不想阻塞线程而坚持使用await来获取选项,你可以使用async await in Program.cs来获取您的选项并将其存储在静态 class 中并在启动时使用它。

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

您的前几个请求无法触发 OnMessageReceived 的原因不是因为您正在使用 async void 委托,而是因为加载参数和附加事件的顺序。

您将处理程序附加到事件 after 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
    });