ASP.NET Core集成Microsoft Entra ID认证时的用户外键处理问题
问题背景
我正在将Microsoft Entra ID(Azure AD)集成到ASP.NET Core应用中做身份认证,由于Entra ID是外部SSO提供商,打算不再在数据库中存储用户数据(即移除原有的User实体)。但现有数据库架构中,Bot、UserExchange、VirtualBalance等实体都依赖User表作为外键关联,认证后应用只能获取JWT中的sub声明作为用户唯一标识,因此遇到两个核心问题:
- 外键问题:移除
User实体后,如何替换关联表中UserId的外键约束? - 查询与关联问题:没有
User实体的前提下,如何不破坏现有关系查询用户相关数据?
解决方案
1. 外键约束的处理
针对外键约束问题,提供两种可行方案:
方案A:保留UserId字段,移除外键约束
将UserId从外键字段改为业务标识字段,直接移除关联表中指向User表的外键约束,既保留数据关联能力,又避免因User表不存在导致的约束错误:
- 数据库层面:删除
Bot、UserExchange、VirtualBalance表中UserId的外键约束。 - EF Core配置:移除实体中
HasOne(x => x.User)的关联配置,保留UserId作为普通字段。
方案B:创建极简UserProfile实体(可选)
如果需要存储少量用户元数据(如首次登录时间),可以创建仅包含Entra ID sub值的极简实体作为关联锚点:
public class UserProfile : BaseAuditableEntity { public UserProfile(string entraSub) { Id = entraSub; // 用Entra ID的sub作为主键(字符串类型) CreatedAt = DateTime.UtcNow; } public override string Id { get; protected set; } // 主键改为字符串类型 }
此时关联表的UserId改为字符串类型,外键指向UserProfile.Id,既保留约束,又不存储冗余用户信息。
2. 查询与关联问题的解决
无论采用哪种方案,都基于当前登录用户的sub声明进行数据关联,以下是具体调整:
调整实体类(以方案A为例)
移除实体中对User导航属性的引用,将UserId类型改为字符串(匹配Entra ID的sub类型):
// 修改Bot类 public class Bot : BaseAuditableEntity { private Bot() { } public Bot(string userId, Guid exchangeId, Pair pair, decimal investment) { Id = Guid.CreateVersion7(); UserId = userId; // 改为字符串类型 ExchangeId = exchangeId; Pair = pair; Investment = investment; } public string UserId { get; private set; } public Guid ExchangeId { get; private set; } public Pair Pair { get; private set; } = null!; public decimal Investment { get; private set; } // 移除原User导航属性 public Exchange Exchange { get; private set; } = null!; } // 修改UserExchange类 public class UserExchange { public UserExchange(string userId, Guid exchangeId, string apiKey, string secretKey, string? passphrase = null) { UserId = userId; // 改为字符串类型 ExchangeId = exchangeId; ApiKey = apiKey; SecretKey = secretKey; Passphrase = passphrase; } public string UserId { get; private set; } public Guid ExchangeId { get; private set; } public string ApiKey { get; private set; } public string SecretKey { get; private set; } public string? Passphrase { get; private set; } // 移除原User导航属性 public Exchange Exchange { get; private set; } = null!; } // 修改VirtualBalance类 public class VirtualBalance : BaseAuditableEntity { public VirtualBalance(string asset, decimal available, decimal locked, string userId) { Id = Guid.CreateVersion7(); Asset = asset; Available = available; Locked = locked; UserId = userId; // 添加userId参数,改为字符串类型 } public string Asset { get; private set; } public decimal Available { get; private set; } public decimal Locked { get; private set; } public string UserId { get; private set; } // 移除原User导航属性 }
修改EF Core配置
移除关联到User表的配置,以UserExchangeConfiguration为例:
public class UserExchangeConfiguration : IEntityTypeConfiguration<UserExchange> { public void Configure(EntityTypeBuilder<UserExchange> builder) { builder.ToTable("UserExchange", SchemaNames.Settings); builder.HasKey(x => new { x.UserId, x.ExchangeId }); builder.Property(x => x.UserId).HasMaxLength(256).IsRequired(); // 字符串类型,设置长度 builder.Property(x => x.ExchangeId).IsRequired(); builder.Property(x => x.ApiKey).HasMaxLength(256).IsRequired(); builder.Property(x => x.SecretKey).HasMaxLength(256).IsRequired(); builder.Property(x => x.Passphrase).HasMaxLength(256).IsRequired(false); // 移除原User关联配置 builder.HasOne(x => x.Exchange) .WithMany() .HasForeignKey(x => x.ExchangeId) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); builder.HasIndex(x => new { x.UserId, x.ExchangeId }).IsUnique(); } }
查询用户数据
从当前用户的Claims中获取sub值,以此作为查询条件:
// 获取当前用户的sub声明 var currentUserSub = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"); // 查询用户的Bot列表 var userBots = await _dbContext.Bots .Where(b => b.UserId == currentUserSub) .Include(b => b.Exchange) .ToListAsync(); // 查询用户的虚拟余额 var userBalances = await _dbContext.VirtualBalances .Where(vb => vb.UserId == currentUserSub) .ToListAsync();
关键注意事项
- 类型匹配:Entra ID的
sub是字符串类型,需将所有关联表的UserId字段从Guid改为string,避免类型不匹配。 - 数据迁移:若有存量数据,需编写迁移脚本将原Guid类型的UserId映射为对应用户的Entra ID
sub值(可通过Email关联映射)。 - 数据一致性:采用方案A时,业务逻辑需确保只有合法的
sub值写入UserId字段,避免脏数据。
内容的提问来源于stack exchange,提问作者nop
相关产品推荐
相关产品推荐

