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 的竞争条件,我们不得不想出别的办法。看来你也得想办法了。
我有一个 .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 的竞争条件,我们不得不想出别的办法。看来你也得想办法了。