在 Asp.Net Core Web API 中使用 MassTransit 消息时如何对用户进行身份验证?

Gav*_*and 5 c# authentication masstransit asp.net-core-webapi

我有几个 Asp.Net Core Web API,它们使用 Bearer 身份验证和IdentityServer4.AccessTokenValidation中间件来检查令牌、对用户进行身份验证并创建声明。这适用于 HTTP 请求。

我正在将这些 API 配置为使用 RabbitMQ 作为传输的 MassTransit 端点(用于发布和使用消息)。我按照此处的说明将 MassTransit 添加到 API 并设置消息使用者。典型的工作流程类似于:

对 API 的 HTTP 请求 > 在 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"];
    }
} 
Run Code Online (Sandbox Code Playgroud)

此时,我可以获取令牌并手动调用 Identity Server 中的 Introspection 端点,但随后我需要:

  1. 每次在每个消费者身上都这样做,然后......
  2. ...手动将这些信息传递给逻辑类等,而不是使用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);
    }
}
Run Code Online (Sandbox Code Playgroud)

...然后将其添加到管道中...

IBusControl CreateBus(IServiceProvider serviceProvider)
{
    return Bus.Factory.CreateUsingRabbitMq(cfg =>
    {
        cfg.Host("rabbitmq://localhost");
        cfg.UseAuthenticationFilter();
        // etc ...
    });
}
Run Code Online (Sandbox Code Playgroud)

这就是我被困的地方。我不知道如何对请求范围内的用户进行身份验证。如果不是 HTTP 请求,我不确定这里的最佳实践是什么。任何建议或指示将不胜感激。谢谢...

Tal*_*aul 4

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

Kevin 的技术是将访问令牌 (JWT) 作为总线消息的属性包含在内,然后使用IdentityModel.

总结一下:

在生产者中:

  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;
Run Code Online (Sandbox Code Playgroud)