【发布时间】:2020-05-20 16:16:50
【问题描述】:
我有几个 Asp.Net Core Web API,它们使用 Bearer 身份验证和 IdentityServer4.AccessTokenValidation 中间件来内省令牌、验证用户并创建声明。这适用于 HTTP 请求。
我正在将这些 API 配置为使用 RabbitMQ 作为传输的 MassTransit 端点(用于发布和消费消息)。我按照说明 here 将 MassTransit 添加到 API 并设置消息使用者。典型的工作流程如下:
对 API 的 HTTP 请求 > 在 MassTransit 上发布消息 > RabbitMQ > 在另一个 API 中使用的消息
我很难理解的是如何在总线上消费消息时创建ClaimsPrincipal,以便我知道代表哪个用户执行操作?如果不是 HTTP 请求,则不会调用 AuthenticationHandler。
到目前为止我所做的尝试:
我想我会通过在消息头中传递一个令牌(和/或单独的声明值)来解决这个问题。当使用 MassTransit.PublishContextExecuteExtensions.Publish 发布消息时,发布部分看起来很容易,就像 MassTransit allows adding any number of custom headers。这使我可以将带有识别用户信息的消息发送到传输中,并且可以通过手动查看标头在消费者中查看此信息,例如
public class SomeEventConsumer : IConsumer<SomeEventData>
{
public async Task Consume(ConsumeContext<SomeEventData> context)
{
var token = context.Headers["token"];
}
}
此时我可以获取令牌并手动调用 Identity Server 中的 Introspection 端点,但我需要:
- 每次都对每个消费者执行此操作,然后...
- ... 手动将该信息传递给逻辑类等,而不是使用
IHttpContextAccessor.HttpContext.User.Claims或包装声明并使用依赖注入。
为了解决第 1 点,我创建了一个新的 custom middleware ...
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 请求,我不确定这里有什么最佳实践。任何建议或指示将不胜感激。谢谢...
【问题讨论】:
-
用户身份验证是网络问题。应该使用您的身份提供者正确完成。您发送到总线的消息可能包含身份本身、范围和子,无论您在声明中获得什么。用户必须已经过身份验证。
-
@AlexeyZimarev 所以我只需要在生产者端序列化委托人的声明并将它们添加到消息头中。在消费者端我只需要阅读它们,反序列化到 ClaimsPrincipal 并设置当前线程。可以吗?
-
@Gavin Sutherland 为什么决定传递一个包含用户声明的令牌?
-
@АлександрСысоев .. 因为我可以通过查询创建它的可信授权服务(自省端点)来验证令牌是否有效。通过这样做,我可以确信令牌中的声明是真实的且没有恶意。例如,如果我刚刚传递了用户声明,我如何确定来自
admin@some.system的消息真的来自那个人? -
@GavinSutherland 您的应用程序在发送消息之前已经对用户进行了身份验证,不是吗?消费者可以假设消息是由受信任的服务发送的吗?这也避免了令牌过期问题(如果令牌在 HTTP 请求期间有效但在消费者可以处理消息之前过期怎么办?)
标签: c# authentication asp.net-core-webapi masstransit