在 DbContext 中为多租户应用程序注入 userid

Injecting userid in DbContext for multi-tenant application

提问人:athagiorgos 提问时间:11/16/2023 更新时间:11/16/2023 访问量:54

问:

我正在构建一个具有 .net 标识的多租户应用程序。我希望每个用户在登录时都能查看自己的数据。我有一个用户解析器服务,我在其中注入 httpContextAccessor 以尝试从 HttpContext 的声明主体注入用户 ID。

internal sealed class AppUserResolver : IAppUserResolver
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public AppUserResolver(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }
    
    public string GetCurrentAppUserId()
    {
        return _httpContextAccessor.HttpContext!.User
            .FindFirstValue(ClaimTypes.NameIdentifier)!;
    }
}

然后,我将服务注入到 DbContext 类中。

public class AppDbContext : IdentityDbContext<AppUser>
{
    private readonly IAppUserResolver _appUserResolver;
    public AppDbContext(DbContextOptions<AppDbContext> options, IAppUserResolver appUserResolver)
        : base(options)
    {
        _appUserResolver = appUserResolver;
    }

    public string GetUserId()
    {
        return _appUserResolver.GetCurrentAppUserId();
    }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        
        modelBuilder.ApplyConfiguration(new UserInfoConfiguration(GetUserId()));
        modelBuilder.ApplyConfiguration(new EmailInfoConfiguration(GetUserId()));
        modelBuilder.ApplyConfiguration(new SocialMediaInfoConfiguration(GetUserId()));
    }
}

我怎么注意到当 DI 容器注入 DbContext 时声明不可用,因此我没有直接在 DbContext 构造函数中获取声明,而是创建了一个方法,仅在需要时获取它们。

但问题出在 OnModleCreating 方法上。此方法仅在应用启动且 EF Core 缓存配置时调用一次。但是在此方法中,我已将 HasQueryFilter 方法应用于某些实体,但是它被设置为空字符串,因为当用户尝试登录时,声明为 null,因此 dbContext 第一次初始化时,它使用 null 用户 ID 进行初始化,OnModelCreating 也是如此。

internal sealed class UserInfoConfiguration : IEntityTypeConfiguration<UserInfo>
{
    private readonly string _appUserId;

    public UserInfoConfiguration(string appUserId)
    {
        _appUserId = appUserId;
    }

    public void Configure(EntityTypeBuilder<UserInfo> builder)
    {
        builder.ToTable("UserInfo");
        
        builder.HasKey(x => x.Id);

        builder.HasOne<AppUser>(x => x.AppUser)
            .WithOne(x => x.UserInfo)
            .HasForeignKey<UserInfo>(x => x.AppUserId)
            .IsRequired();

        builder.HasQueryFilter(x => x.AppUserId == _appUserId);
    }
}

登录后,声明主体可用于每个用户操作,但 OnModelCreating 已运行并缓存。因此,之后进行的查询是错误的,并且由于它们使用 mepty 字符串作为用户 ID 进行过滤,因此会带来 nonthing。关于如何解决这个问题的任何建议,或者我应该使用用户 ID 过滤任何地方?

我还在服务注册时注册了 HttpContextAccessor 和我的用户解析器服务

 serviceCollection.AddHttpContextAccessor();
 serviceCollection.AddScoped<IAppUserResolver, AppUserResolver>();
实体框架 asp.net-core 依赖注入 net-6.0

评论

0赞 D A 11/16/2023
这里有一个很好的示例,说明如何构建多租户应用 code-maze.com/aspnetcore-multitenant-application
0赞 athagiorgos 11/16/2023
这是我遵循的示例,我遇到了上面描述的问题。OnModelCreating 使用空 userId 进行初始化,因为当用户登录时,声明为 null,因此没有用户 ID,但必须初始化 db comntext,以便登录方法访问 users 表
0赞 D A 11/16/2023
你读过这个吗?“通常,租户设置来自单独的数据库或注册表系统。但是我们将通过使用 appsettings.json 配置文件来保持简单:”
0赞 athagiorgos 11/16/2023
因此,您不能将这种查询过滤方法与标识和表的其余部分放在同一个 DbContext 中吗?该应用程序是一个 mvc 应用程序

答:

0赞 Emre Bener 11/16/2023 #1

实现依赖于 HttpContextAccessor 的自定义服务并将其注入 DbContext 似乎没有必要。在我看来,你似乎在试图重新发明轮子。为什么不简单地在控制器中使用属性呢?授权中间件将自动使用当前用户的声明填充此属性(如果使用 cookie authn 方案,则从 cookie 中读取)。User

您可以使用 方便地检索所有用户声明。 用于代码重用。您可以实现这样的帮助程序方法,您可以从控制器调用该方法:User.Identity.Claims

    public static string GetCurrentUserName(ClaimsPrincipal user)
        =>
        !string.IsNullOrWhiteSpace(user?.Identity?.Name) && user.Identity.IsAuthenticated
        ? user.Identity.Name
        : "";

此示例仅返回用户名,但您可以对其进行编辑以返回您愿意检索的确切声明。 注意:对于这种方法,您需要将相关属性注册为用户声明,以便它位于表中,这可确保信息将作为声明存储在 cookie 中。AspNetUserClaims

例如,下面介绍如何将声明分配给用户:

var someUser = userMgr.FindByNameAsync("JohnWick").Result;
userMgr.AddClaimsAsync(someUser, new Claim[]{
                                        new Claim("Company", "Mercedes")
                                    }).Result;

注意:这是声明方法,但你也可以选择使用角色(这些角色也几乎是声明tbh,只是灵活性稍差,并且你使用不同的标识方法)。如果您不想将此信息保留在 cookie 中,那么对于每个请求,您都必须从用户表的数据库中读取属性,这不是一种高性能的方法,因此我建议不要这样做。

评论

0赞 athagiorgos 11/17/2023
每个展示如何实现多租户的例子都遵循这种特定的方法,所以我认为没有人试图重新发明轮子。问题可能是因为注入 dbContext 的最长时间,in被初始化为空字符串,因为当时用户尝试登录,没有额外的 appuserid。也许在 evcery 控制器中注入用户 ID 是这里唯一的方法
0赞 Emre Bener 11/17/2023
@athagiorgos将用户 ID 注入到每个控制器?我不确定你的意思。正如我在回答中解释的那样,属性由授权中间件自动初始化,该中间件可在所有控制器中访问。此属性包含有关当前用户的信息。User
0赞 Emre Bener 11/17/2023
尝试在每个请求中使用当前用户的 ID 实例化 DbContext 并不是解决此问题的方法。我知道您在使用服务容器时遇到了问题,但您真的不需要做您正在尝试做的事情。我可以帮助您解决该特定问题,但这不是一个正确的解决方案,并且可能会导致更多问题。您需要访问当前用户信息,我解释了适当的方法。请尝试我的建议。
0赞 athagiorgos 11/20/2023
我在你的例子中使用了你的逻辑。这也是我的第一个想法。我创建了一个扩展方法来从 httpcontext 返回用户 ID,并且它正在工作。只是使用这种方法,我必须链接一个.Where() 在我对数据库进行的每个 linq 查询中,我只想用 HasQueryFilter 方法包装它。