You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET Core集成Microsoft Entra ID认证时的用户外键处理问题

问题背景

我正在将Microsoft Entra ID(Azure AD)集成到ASP.NET Core应用中做身份认证,由于Entra ID是外部SSO提供商,打算不再在数据库中存储用户数据(即移除原有的User实体)。但现有数据库架构中,Bot、UserExchange、VirtualBalance等实体都依赖User表作为外键关联,认证后应用只能获取JWT中的sub声明作为用户唯一标识,因此遇到两个核心问题:

  1. 外键问题:移除User实体后,如何替换关联表中UserId的外键约束?
  2. 查询与关联问题:没有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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 22:02:32