我正在构建一个具有 .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 方法,但是它设置为空字符串,因为当用户尝试登录时,声明为空,因此 dbContext 第一次初始化时,它会使用空用户 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 已运行并已缓存。因此,之后进行的查询是错误的,并且不会带来任何结果,因为它们使用简单的字符串作为用户 ID 进行过滤。关于如何解决这个问题有什么建议,或者我应该使用用户 ID 过滤所有地方吗?
我还在服务注册处注册了 HttpContextAccessor 和我的用户解析器服务
serviceCollection.AddHttpContextAccessor();
serviceCollection.AddScoped<IAppUserResolver, AppUserResolver>();
实现依赖于 HttpContextAccessor 的自定义服务并将其注入 DbContext 对我来说似乎没有必要。为什么不在控制器中简单地使用
User
属性呢?授权中间件将自动使用当前用户的声明填充此属性(如果使用 cookie 身份验证方案,则从 cookie 读取)。
您可以使用
User.Identity.Claims
方便地检索所有用户声明。
用于代码重用。您可以实现这样的辅助方法,您可以从控制器调用该方法:
public static string GetCurrentUserName(ClaimsPrincipal user)
=>
!string.IsNullOrWhiteSpace(user?.Identity?.Name) && user.Identity.IsAuthenticated
? user.Identity.Name
: "";
此示例仅返回用户名,但您可以对其进行编辑以返回您愿意检索的确切声明。
注意:对于这种方法,您需要将相关属性注册为用户声明。这样它就在
AspNetUserClaims
表中(这是默认名称,假设您没有配置该名称)。
例如,以下是如何向用户分配声明的方法:
var someUser = userMgr.FindByNameAsync("JohnWick").Result;
userMgr.AddClaimsAsync(someUser, new Claim[]{
new Claim("Company", "Mercedes")
}).Result;
注意:这是声明方法,但您也可以选择使用角色(这也几乎是声明,只是灵活性稍差一些,并且您使用不同的身份方法)。 如果您不想将此信息保留在 cookie 中,那么您将必须为每个请求从用户表的数据库中读取属性,这不是一个非常高效的方法,我建议不要这样做。
注意:多租户实际上只是意味着隔离系统中的用户组,因此这方面的实现取决于您。问题是关于确定用户属于哪个组。