.NET Core3.1 IdentityServer4配置邮箱非唯一仍报DuplicateEmail错误
问题根因
options.User.RequireUniqueEmail = false 仅关闭了Identity业务层的重复邮箱校验逻辑,不会自动修改数据库层已经生成的唯一索引。
Identity在首次通过EF Core迁移生成用户表结构时,默认会给NormalizedEmail字段创建单独的唯一索引。如果你是在先启用了默认唯一邮箱配置的情况下生成的数据库,后续仅修改Startup配置不会自动删除这个已存在的索引,因此哪怕业务层校验放行同邮箱不同用户名的用户,写入数据库时依然会触发重复键冲突,抛出你看到的Email 'xxx' is already taken错误。
解决方案
不需要完全重写Identity的整套逻辑,根据你的业务需求选以下任意一种方案即可:
方案1:适配「用户名+邮箱」组合唯一的业务规则(推荐)
按顺序操作即可彻底解决问题:
- 修改DbContext的模型配置,替换默认索引
在AdminIdentityDbContext中重写OnModelCreating方法,移除默认的单邮箱唯一索引,新增用户名+邮箱的组合唯一索引:protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); // 移除NormalizedEmail字段的默认唯一索引,索引名和你现有数据库中的实际名称保持一致 builder.Entity<UserIdentity<string>>() .HasIndex(u => u.NormalizedEmail) .IsUnique(false) .HasDatabaseName("IX_AspNetUsers_NormalizedEmail"); // 新增归一化用户名+归一化邮箱的组合唯一索引,匹配你的业务规则 builder.Entity<UserIdentity<string>>() .HasIndex(u => new { u.NormalizedUserName, u.NormalizedEmail }) .IsUnique() .HasDatabaseName("IX_AspNetUsers_NormalizedUserName_NormalizedEmail"); } - 生成新的EF Core迁移脚本并执行到数据库,完成索引替换
- 修正你自己写的前置重复校验逻辑,改用归一化后的值做对比
Identity存储用户名、邮箱时会做大写归一化处理,直接对比原始值会漏过大写差异的重复数据(比如Test@Gmail.com和test@gmail.com会被误判为不同值),修正后的校验逻辑如下:var normalizedUserName = _userManager.NormalizeName(model.UserName); var normalizedEmail = _userManager.NormalizeEmail(model.Email); bool combinationExists = await _userManager.Users .AnyAsync(x => x.NormalizedUserName == normalizedUserName && x.NormalizedEmail == normalizedEmail && x.Id != user.Id); // 加Id判断避免用户更新信息时误判自己和自己重复
方案2:自定义用户校验器替换默认逻辑
如果暂时不方便修改数据库结构,可以通过替换内置校验器的方式调整规则,注意最终还是要删除数据库里的NormalizedEmail唯一索引,否则写入时依然会报错:
- 自定义校验类继承默认的
UserValidator,重写邮箱校验逻辑:public class CompositeUniqueUserValidator<TUser> : UserValidator<TUser> where TUser : UserIdentity<string> { public override async Task<IdentityResult> ValidateAsync(UserManager<TUser> manager, TUser user) { var errors = new List<IdentityError>(); // 保留默认的用户名合法性校验 await ValidateUserName(manager, user, errors); // 替换默认单邮箱校验为组合唯一校验 var normalizedEmail = manager.NormalizeEmail(await manager.GetEmailAsync(user)); var normalizedUserName = manager.NormalizeName(await manager.GetUserNameAsync(user)); var duplicateUser = await manager.Users .FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail && u.NormalizedUserName == normalizedUserName && u.Id != user.Id); if (duplicateUser != null) { errors.Add(new IdentityError { Code = "DuplicateUserNameEmailCombination", Description = $"用户名{user.UserName}与邮箱{user.Email}的组合已存在" }); } return errors.Any() ? IdentityResult.Failed(errors.ToArray()) : IdentityResult.Success; } } - 在Startup的Identity配置中注册自定义校验器,替换默认实现:
services.AddIdentity<UserIdentity<string>, UserIdentityRole>(options => { options.User.RequireUniqueEmail = false; }) .AddEntityFrameworkStores<AdminIdentityDbContext>() .AddUserValidator<CompositeUniqueUserValidator<UserIdentity<string>>>() .AddDefaultTokenProviders();
排查优先级
优先先连数据库查看用户表的索引配置,90%以上的这类问题都是旧的唯一邮箱索引没有删除导致的,不需要一开始就重写Identity的整套默认逻辑。
内容的提问来源于stack exchange,提问作者Iswar
相关产品推荐
相关产品推荐

