【问题标题】:JwtBearerEvents.OnMessageReceived not Called for First Operation Invocation首次操作调用未调用 JwtBearerEvents.OnMessageReceived
【发布时间】:2020-07-23 12:31:28
【问题描述】:

我使用 WSO2 作为我的身份提供者 (IDP)。它将 JWT 放在名为“X-JWT-Assertion”的标头中。

为了将其输入 ASP.NET Core 系统,我添加了一个 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 被调用来为我的服务首次调用服务操作?

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

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

【问题讨论】:

  • 我已将此事件处理程序放入 bolierplate WebAPI 模板 - 似乎从第一个请求中提取处理程序。会不会是您的中间件订单在某种程度上影响了它?
  • @timur - 在我的问题末尾查看我的更新。 (这是由于 async await 无法很好地与调用管道配合使用。)
  • 似乎AddJwtBearer(和底层的AuthenticationBuilder.AddSchemeHelper)不期望在那里进行异步调用 - 它只是将 IConfigureOptions 添加到服务中。另一方面,@ 987654346@ - 正在等待中。所以我想知道你是否可以让OnMessageReceived lambda 异步,将你的 http 调用移动到OnMessageReceived 正文并以某种方式将结果缓存在那里?
  • @Vaccano 你有没有想出一个好的解决方案来解决实际的原始问题?好吧,@timur 的解决方案可能适用于您的情况,但不适用于我的情况。我需要将options.Authority 设置为异步获得的值,但我想不出比options.Authority = Task.Run(() => ...).GetAwaiter().GetResult() 更好的值,但我想尽可能避免将异步代码包装在同步代码中。不过我希望不会。
  • @jsabrooke - 是的,我做到了。我能够使用依赖注入让它工作。这样我就可以提前设置我需要的东西。要让它工作,你需要像这样注册一个IConfigureOptionsservices.AddTransient<IConfigureOptions<JwtBearerOptions>, ConfigureJwtBearerOptions>(); 然后创建一个继承自IConfigureNamedOptions<JwtBearerOptions> 的类(在这种情况下称为ConfigureJwtBearerOptions)。该类将用于设置 JwtBearerOptions 并将支持依赖注入。这是一个例子:stackoverflow.com/q/61186836/16241

标签: c# asp.net-core .net-core asp.net-core-3.1


【解决方案1】:

更新:
lambda 是一个Action 方法。它不返回任何东西。因此,如果不火而忘记,就不可能在其中尝试异步。

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


这是我终于想通了。

AddJwtBearer 的 lambda 不适用于 async await。我对 await wso2Actions.JwtOperations.GetTokenValidationParameters(); 的呼叫等待正常,但呼叫管道继续进行,无需等待 AddJwtBearer 完成。

asyncawait 调用顺序如下:

  1. 服务启动(您等待一段时间让一切顺利。)
  2. 调用服务。
  3. AddJwtBearer 被调用。
  4. await wso2Actions.JwtOperations.GetTokenValidationParameters(); 被调用。
  5. GetTokenValidationParameters() 使用 await 调用 HttpClient
  6. HttpClient 进行等待调用以获取颁发者的公共签名密钥。
  7. HttpClient 正在等待时,原始呼叫的其余部分将通过。尚未设置任何事件,因此它只是像往常一样继续调用管道。
    • 这是它“似乎跳过”OnMessageReceived 事件的地方。
  8. HttpClient 使用公钥获取响应。
  9. AddJwtBearer 继续执行。
  10. OnMessageReceived 事件已设置。
  11. 第二次调用服务
  12. 因为最终设置了事件,所以调用了该事件。 (AddJwtBearer 仅在第一次调用时调用。)

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

我将 AddJwtBearer 的 lambda 更改为不异步,它工作得很好。

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

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

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

【讨论】:

  • 您在那里创建了竞态条件。您可以在第 4 步之前执行第 10 步来解决此问题。看我的回答:)
  • @weichch - 不幸的是,需要等待的调用才能解码 JWT。如果我按照您显示的方式订购,第一次调用将无法通过令牌验证。
  • 对不起,我不知道还有另一个比赛条件 :) 尝试更新。最初的答案是等待加载参数的相同任务,这不应该是 OnMessageReceived 使用的任务。
【解决方案2】:

您的前几个请求无法触发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
    });

【讨论】:

    【解决方案3】:

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

    但是,如果您不喜欢阻塞线程并坚持使用await 来获取选项,您可以使用Program.cs 中的await 来获取您的选项并将其存储在静态类中并在启动时使用它。

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

    【讨论】:

      猜你喜欢
      • 2016-08-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-02-06
      相关资源
      最近更新 更多