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
中。 (请参阅下面我的回答。)这就是我真正想从这个问题中得到的。
但是,由于无法取消赏金,我仍然会将赏金奖励给任何可以展示使用 AddJwtBearer
和 async
await
的方法的人,它正在等待实际 HttpClient
调用。或者显示为什么 async
await
不应该与 AddJwtBearer
.
一起使用的文档
更新:
lambda 是一种 Action
方法。它没有 return 任何东西。因此,如果不触发并忘记它,就不可能尝试在其中进行异步操作。
此外,此方法在第一次调用时被调用。所以答案是提前在这个方法中调用你需要的任何东西并缓存它。 (但是,我还没有想出一种使用依赖项注入项进行此调用的非黑客方法。)然后在第一次调用期间,将调用此 lambda。那时你应该从缓存中提取你需要的值(这样不会减慢第一次调用的速度)。
这是我终于想通的。
AddJwtBearer
的 lambda 不适用于 async
await
。我对 await wso2Actions.JwtOperations.GetTokenValidationParameters();
的调用等待得很好,但是调用管道继续进行而不等待 AddJwtBearer
完成。
使用 async
await
调用顺序如下:
- 服务启动(您稍等片刻,一切顺利。)
- 调用服务。
AddJwtBearer
被调用。
await wso2Actions.JwtOperations.GetTokenValidationParameters();
被调用。
GetTokenValidationParameters()
用 await
调用 HttpClient
。
HttpClient
执行等待调用以获取发行者的 public 签名密钥。
- 在
HttpClient
等待期间,原始呼叫的其余部分将继续进行。还没有设置任何事件,所以它只是像往常一样继续调用管道。
- 这是 "appears to skip"
OnMessageReceived
事件的地方。
HttpClient
使用 public 键获得响应。
AddJwtBearer
的执行继续。
OnMessageReceived
事件已设置。
- 第二次调用该服务
- 因为最终设置了事件,所以调用了事件。 (
AddJwtBearer
仅在第一次调用时被调用。)
因此,当等待发生时(在这种情况下,它最终会调用 HttpClient 以获取颁发者签名密钥),第一个调用的其余部分将通过。因为还没有事件设置,它不知道调用处理程序。
我将 AddJwtBearer
的 lambda 更改为非异步并且它工作得很好。
备注:
这里有两件事看起来很奇怪:
- 我本以为
AddJwtBearer
会在启动时被调用,而不是在第一次调用服务时被调用。
- 如果
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
});
我正在使用 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
中。 (请参阅下面我的回答。)这就是我真正想从这个问题中得到的。
但是,由于无法取消赏金,我仍然会将赏金奖励给任何可以展示使用 AddJwtBearer
和 async
await
的方法的人,它正在等待实际 HttpClient
调用。或者显示为什么 async
await
不应该与 AddJwtBearer
.
更新:
lambda 是一种 Action
方法。它没有 return 任何东西。因此,如果不触发并忘记它,就不可能尝试在其中进行异步操作。
此外,此方法在第一次调用时被调用。所以答案是提前在这个方法中调用你需要的任何东西并缓存它。 (但是,我还没有想出一种使用依赖项注入项进行此调用的非黑客方法。)然后在第一次调用期间,将调用此 lambda。那时你应该从缓存中提取你需要的值(这样不会减慢第一次调用的速度)。
这是我终于想通的。
AddJwtBearer
的 lambda 不适用于 async
await
。我对 await wso2Actions.JwtOperations.GetTokenValidationParameters();
的调用等待得很好,但是调用管道继续进行而不等待 AddJwtBearer
完成。
使用 async
await
调用顺序如下:
- 服务启动(您稍等片刻,一切顺利。)
- 调用服务。
AddJwtBearer
被调用。await wso2Actions.JwtOperations.GetTokenValidationParameters();
被调用。GetTokenValidationParameters()
用await
调用HttpClient
。HttpClient
执行等待调用以获取发行者的 public 签名密钥。- 在
HttpClient
等待期间,原始呼叫的其余部分将继续进行。还没有设置任何事件,所以它只是像往常一样继续调用管道。- 这是 "appears to skip"
OnMessageReceived
事件的地方。
- 这是 "appears to skip"
HttpClient
使用 public 键获得响应。AddJwtBearer
的执行继续。OnMessageReceived
事件已设置。- 第二次调用该服务
- 因为最终设置了事件,所以调用了事件。 (
AddJwtBearer
仅在第一次调用时被调用。)
因此,当等待发生时(在这种情况下,它最终会调用 HttpClient 以获取颁发者签名密钥),第一个调用的其余部分将通过。因为还没有事件设置,它不知道调用处理程序。
我将 AddJwtBearer
的 lambda 更改为非异步并且它工作得很好。
备注:
这里有两件事看起来很奇怪:
- 我本以为
AddJwtBearer
会在启动时被调用,而不是在第一次调用服务时被调用。 - 如果
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
});