我有几个 Asp.Net 核心 Web API,它们使用持有者身份验证和IdentityServer4.AccessTokenValidation
中间件来内省令牌、对用户进行身份验证和创建声明。这适用于 HTTP 请求。
我正在将这些 API 配置为使用 RabbitMQ 作为传输的 MassTransit 端点(用于发布和使用消息(。我按照此处的说明将 MassTransit 添加到 API 并设置消息使用者。典型的工作流程如下:
HTTP 请求 API> 在 MassTransit 上发布消息> RabbitMQ> 在另一个 API 中使用的消息
我正在努力理解的是,在公交车外使用消息时,如何创建ClaimsPrincipal
,以便我知道代表哪个用户执行操作?如果它不是 HTTP 请求,则不会调用 AuthenticationHandler。
到目前为止我尝试过:
我想我会通过在消息标头中传递令牌(和/或单个声明值(来解决这个问题。发布部分似乎很容易,因为 MassTransit 允许在使用MassTransit.PublishContextExecuteExtensions.Publish
发布消息时添加任意数量的自定义标头。这允许我将带有标识用户的信息的消息发送到传输中,并且可以通过手动查看标头在消费者中查看此信息,例如
public class SomeEventConsumer : IConsumer<SomeEventData>
{
public async Task Consume(ConsumeContext<SomeEventData> context)
{
var token = context.Headers["token"];
}
}
此时,我可以获取令牌并手动调用身份服务器中的自检端点,但随后我需要:
- 每次都在每个消费者身上这样做,然后......
- 。手动将该信息传递给逻辑类等,而不是使用
IHttpContextAccessor.HttpContext.User.Claims
或通过包装声明并使用依赖注入。
为了解决第 1 点,我创建了一个新的自定义中间件...
public class AuthenticationFilter<T> : IFilter<ConsumeContext<T>> where T : class
{
public void Probe(ProbeContext context)
{
var scope = context.CreateFilterScope("authenticationFilter");
}
public async Task Send(ConsumeContext<T> context, IPipe<ConsumeContext<T>> next)
{
var token = context.Headers.Where(x => x.Key == "token").Select(x => x.Value.ToString()).Single();
// TODO: Call token introspection
await next.Send(context);
}
}
public class AuthenticationFilterSpecification<T> : IPipeSpecification<ConsumeContext<T>> where T : class
{
public void Apply(IPipeBuilder<ConsumeContext<T>> builder)
{
var filter = new AuthenticationFilter<T>();
builder.AddFilter(filter);
}
public IEnumerable<ValidationResult> Validate()
{
return Enumerable.Empty<ValidationResult>();
}
}
public class AuthenticationFilterConfigurationObserver : ConfigurationObserver, IMessageConfigurationObserver
{
public AuthenticationFilterConfigurationObserver(IConsumePipeConfigurator receiveEndpointConfigurator) : base(receiveEndpointConfigurator)
{
Connect(this);
}
public void MessageConfigured<TMessage>(IConsumePipeConfigurator configurator)
where TMessage : class
{
var specification = new AuthenticationFilterSpecification<TMessage>();
configurator.AddPipeSpecification(specification);
}
}
public static class AuthenticationExtensions
{
public static void UseAuthenticationFilter(this IConsumePipeConfigurator configurator)
{
if (configurator == null)
{
throw new ArgumentNullException(nameof(configurator));
}
_ = new AuthenticationFilterConfigurationObserver(configurator);
}
}
。然后将其添加到管道中...
IBusControl CreateBus(IServiceProvider serviceProvider)
{
return Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.Host("rabbitmq://localhost");
cfg.UseAuthenticationFilter();
// etc ...
});
}
这就是我卡住的地方。我不知道如何在请求范围内对用户进行身份验证。如果它不是HTTP请求,我不确定这里的最佳实践是什么。任何建议或指示将不胜感激。谢谢。。。
我刚刚在观看 Pluralsight 上的 Kevin Dockx 课程,该课程涵盖了 Azure 服务总线上的这种情况,但相同的原则适用于公共交通或使用消息总线的服务之间的任何其他异步通信。以下是以下部分的链接:保护核心 ASP.NET 中的微服务
Kevin的技术是将访问令牌(JWT(作为属性包含在总线消息中,然后使用IdentityModel
在使用者中验证这一点。
总结一下:
在制作人中:
- 从请求中获取访问令牌(例如
HttpContext.GetUserAccessTokenAsync()
(。 - 在发送之前将其设置为消息中的属性。
在消费者中:
- 使用
IdentityModel
获取 IdP 发现文档 - 从发现响应中提取公共签名密钥(这些密钥必须转换为
RsaSecurityKey
( - 调用
JwtSecurityTokenHandler.ValidateToken()
以验证消息中的 JWT。如果成功,这将返回一个ClaimsPrincipal
。
如果担心访问令牌过期,可以使用消息排队的日期时间作为使用者中的令牌验证逻辑的一部分。
以下是验证器的工作原理(简化(:
var discoveryDocumentResponse = await httpClient.GetDiscoveryDocumentAsync("https://my.authority.com");
var issuerSigningKeys = new List<SecurityKey>();
foreach (var webKey in discoveryDocumentResponse.KeySet.Keys)
{
var e = Base64Url.Decode(webKey.E);
var n = Base64Url.Decode(webKey.N);
var key = new RsaSecurityKey(new RSAParameters
{ Exponent = e, Modulus = n })
{
KeyId = webKey.Kid
};
issuerSigningKeys.Add(key);
}
var tokenValidationParameters = new TokenValidationParameters()
{
ValidAudience = "my-api-audience",
ValidIssuer = "https://my.authority.com",
IssuerSigningKeys = issuerSigningKeys
};
var claimsPrincipal = new JwtSecurityTokenHandler().ValidateToken(tokenToValidate,
tokenValidationParameters, out var rawValidatedToken);
return claimsPrincipal;