【问题标题】:How to authenticate a user when consuming MassTransit messages in Asp.Net Core Web API?在 Asp.Net Core Web API 中消费 MassTransit 消息时如何对用户进行身份验证?
【发布时间】: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 端点,但我需要:

  1. 每次都对每个消费者执行此操作,然后...
  2. ... 手动将该信息传递给逻辑类等,而不是使用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


【解决方案1】:

我刚刚在 Pluralsight 上观看了 Kevin Dockx 课程,该课程涵盖了 Azure 服务总线上的这种场景,但同样的原则也适用于公共交通或使用消息总线的服务之间的任何其他异步通信。这是该部分的链接:Securing Microservices in ASP.NET Core

Kevin 的技术是将访问令牌 (JWT) 作为属性包含在总线消息中,然后使用 IdentityModel 在消费者中验证这一点。

总结一下:

在 Producer 中:

  1. 从请求中获取访问令牌(例如HttpContext.GetUserAccessTokenAsync())。
  2. 在发送前将此设置为消息中的属性。

在消费者中:

  1. 使用 IdentityModel 获取 IdP 发现文档
  2. 从发现响应中提取公共签名密钥(这些必须转换为 RsaSecurityKey
  3. 调用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;

【讨论】:

    猜你喜欢
    • 2013-11-14
    • 2018-07-02
    • 2013-11-16
    • 1970-01-01
    • 2016-12-22
    • 1970-01-01
    • 2023-03-05
    • 2022-11-21
    • 2017-07-04
    相关资源
    最近更新 更多