Autofac 多租户从当前用户的主体中解析租户

Autofac multitenancy resolve tenant from current user’s principal

我有一个 .NET 5 ASP.NET Core API,我在其中尝试使用 Autofac.AspNetCore.Multitenant v4.0.1 设置多租户。我已经实施了一个 TenantIdentificationStrategy,它根据当前用户委托人的声明来识别租户。 问题是在 Autofac 解析租户标识符时用户似乎尚未填充。

Autofac 文档在这里 https://autofaccn.readthedocs.io/en/latest/advanced/multitenant.html#tenant-identification 状态:

The ITenantIdentificationStrategy allows you to retrieve the tenant ID from anywhere appropriate to your application: an environment variable, a role on the current user’s principal, an incoming request value, or anywhere else.

由于这个声明,我认为这应该可行,所以我想知道我是否做错了什么或者框架是否存在错误,或者文档是否说明了不受支持的内容由框架。

知道如何让它工作吗?

在我的应用程序配置下方找到:

计划

private static IHostBuilder CreateHostBuilder(string[] args)
    => Host.CreateDefaultBuilder(args)
        .UseServiceProviderFactory(new AutofacMultitenantServiceProviderFactory(Startup.ConfigureMultitenantContainer))
        .ConfigureWebHostDefaults(webHostBuilder =>
        {
            webHostBuilder.UseStartup<Startup>();
        });

启动

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            // [...]
        });

    services.AddControllers();

    services.AddAutofacMultitenantRequestServices();
}

public void ConfigureContainer(ContainerBuilder builder)
{
    builder.RegisterType<ClaimTenantIdentificationStrategy>()
        .As<ITenantIdentificationStrategy>()
        .SingleInstance();

    builder.RegisterType<SomeService>()
        .As<ISomeService>()
        .InstancePerTenant();
}

public static MultitenantContainer ConfigureMultitenantContainer(IContainer container)
    => new MultitenantContainer(container.Resolve<ITenantIdentificationStrategy>(), container);

public void Configure(IApplicationBuilder app)
{
    app.UseRouting();
    
    app.UseAuthentication();
    app.UseAuthorization();
    
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

TenantIdentificationStrategy 实施

public class ClaimTenantIdentificationStrategy : ITenantIdentificationStrategy
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ClaimTenantIdentificationStrategy(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
    }

    public bool TryIdentifyTenant(out object tenantId)
    {
        tenantId = null;

        var claimsPrincipal = _httpContextAccessor.HttpContext?.User;
        var claimsIdentity = claimsPrincipal?.Identity;

        if (claimsIdentity != null && claimsIdentity.IsAuthenticated)
        {
            var identifier = claimsPrincipal.FindFirst("tenantId")?.Value;

            if (!string.IsNullOrEmpty(identifier))
                tenantId = identifier;
        }

        return tenantId != null;
    }
}

编辑:TenantIdentificationStrategy

public class ClaimTenantIdentificationStrategy : ITenantIdentificationStrategy
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ITenantStore _tenantStore;

    public ClaimTenantIdentificationStrategy(IHttpContextAccessor httpContextAccessor, ITenantStore tenantStore)
    {
        _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
        _tenantStore = tenantStore ?? throw new ArgumentNullException(nameof(tenantStore));
    }

    public bool TryIdentifyTenant(out object tenantId)
    {
        var httpContext = _httpContextAccessor.HttpContext;

        tenantId = httpContext?.Items["TenantId"];

        if (tenantId != null)
            return true;

        var authorizationHeaderValue = httpContext?.Request?.Headers?[HeaderNames.Authorization];

        if (authorizationHeaderValue.HasValue && !StringValues.IsNullOrEmpty(authorizationHeaderValue.Value))
        {
            var authorizationHeader = AuthenticationHeaderValue.Parse(authorizationHeaderValue);

            var jwtTokenHandler = new JwtSecurityTokenHandler();
            var jwtToken = jwtTokenHandler.ReadJwtToken(authorizationHeader.Parameter);

            var tenantIdClaim = jwtToken.Claims
                .FirstOrDefault(x => x.Type == "tenantId");

            if (tenantIdClaim != null && _tenantStore.IsValidTenant(tenantIdClaim.Value))
            {
                tenantId = tenantIdClaim.Value;
                httpContext.Items["TenantId"] = tenantId;

                return true;
            }
        }

        return false;
    }
}

阅读 Autofac 文档时需要考虑的一点是,Autofac 支持 任何 应用程序类型 - 控制台应用程序、Windows 服务、ASP.NET 应用程序,等等。所以当你看到租户可以来自用户的委托人时……好吧,它可以。 您的应用程序当时是否​​准备就绪取决于您的应用程序。控制台可能会从当前正在执行的线程中获取主体,并且当应用 运行s.

时它已经存在了

对于 ASP.NET / ASP.NET 核心应用程序,中间件 运行 用于验证入站令牌,将该令牌转换为主体,并将该主体附加到当前请求.

但是如果你考虑一下......中间件具有需要从请求容器中解析的依赖关系,在多租户系统中,这意味着我们首先需要了解租户 在中间件管道中,不要等到 authentication/authorization 位 运行.

这也是 ASP.NET 经典版中的一个问题。 ASP.NET Core 并不陌生。我也用自己的应用程序实现了它。

你如何解决这个问题在很大程度上取决于应用程序——你使用的是什么类型的令牌,验证令牌的成本是多少,你想在安全方面承担多大的风险,等等。

在我最近的 运行-in 中,我们使用了签名(但未加密)的 JWT。其中一项声明 tid 包含租户 ID。我们所做的是决定对令牌进行一些手动操作——验证签名,然后直接从令牌中获取 tid 声明。没有更多的解析,没有其他验证(观众等)。它需要快速。然后我们将租户 ID 缓存在 HttpContext.Items 中,这样我们就不必再次查找它,因此它可以在我们需要 运行 身份验证中间件之前的租户 ID 的任何其他地方使用。

有人可以让不同租户的管道从初始请求执行到身份验证中间件,然后被身份验证中间件拒绝,这存在轻微的风险。我们对此没有意见,因为它仍然很快就会拒绝未经授权的人。这可能适合您,也可能不适合您,因此您必须做出决定。

但是,长话短说:由于在身份验证中间件 运行 之前需要租户 ID 的竞争条件,我们不得不想出别的办法。看来你也得想办法了。