Microsoft ASP.NET Identity - Multiple Users with the same name

后端 未结 2 740
面向向阳花
面向向阳花 2020-12-15 12:11

I\'m trying something quite exotic I believe and I\'m facing a few problems, which I hope can be solved with the help of the users here on StackOverflow.

The story<

相关标签:
2条回答
  • 2020-12-15 12:39

    May be someone can find this helpful. In our project we use ASP.NET identity 2 and some day we came across case where two users have identical names. We use emails as logins in our app and they, indeed, have to be unique. But we don't want to have user names unique anyway. What we did just customized few classes of identity framework as follows:

    1. Changed our AppIdentityDbContext by creating index on UserName field as non-unique and override ValidateEntity in tricky way. And then using migrations update database. Code looks like:

      public class AppIdentityDbContext : IdentityDbContext<AppUser>
      {
      
      public AppIdentityDbContext()
          : base("IdentityContext", throwIfV1Schema: false)
      {
      }
      
      protected override void OnModelCreating(DbModelBuilder modelBuilder)
      {
          base.OnModelCreating(modelBuilder); // This needs to go before the other rules!
      
           *****[skipped some other code]*****
      
          // In order to support multiple user names 
          // I replaced unique index of UserNameIndex to non-unique
          modelBuilder
          .Entity<AppUser>()
          .Property(c => c.UserName)
          .HasColumnAnnotation(
              "Index", 
              new IndexAnnotation(
              new IndexAttribute("UserNameIndex")
              {
                  IsUnique = false
              }));
      
          modelBuilder
              .Entity<AppUser>()
              .Property(c => c.Email)
              .IsRequired()
              .HasColumnAnnotation(
                  "Index",
                  new IndexAnnotation(new[]
                  {
                      new IndexAttribute("EmailIndex") {IsUnique = true}
                  }));
      }
      
      /// <summary>
      ///     Override 'ValidateEntity' to support multiple users with the same name
      /// </summary>
      /// <param name="entityEntry"></param>
      /// <param name="items"></param>
      /// <returns></returns>
      protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry,
          IDictionary<object, object> items)
      {
          // call validate and check results 
          var result = base.ValidateEntity(entityEntry, items);
      
          if (result.ValidationErrors.Any(err => err.PropertyName.Equals("User")))
          {
              // Yes I know! Next code looks not good, because I rely on internal messages of Identity 2, but I should track here only error message instead of rewriting the whole IdentityDbContext
      
              var duplicateUserNameError = 
                  result.ValidationErrors
                  .FirstOrDefault(
                  err =>  
                      Regex.IsMatch(
                          err.ErrorMessage,
                          @"Name\s+(.+)is\s+already\s+taken",
                          RegexOptions.IgnoreCase));
      
              if (null != duplicateUserNameError)
              {
                  result.ValidationErrors.Remove(duplicateUserNameError);
              }
          }
      
          return result;
      }
      }
      
    2. Create custom class of IIdentityValidator<AppUser> interface and set it to our UserManager<AppUser>.UserValidator property:

      public class AppUserValidator : IIdentityValidator<AppUser>
      {
      /// <summary>
      ///     Constructor
      /// </summary>
      /// <param name="manager"></param>
      public AppUserValidator(UserManager<AppUser> manager)
      {
          Manager = manager;
      }
      
      private UserManager<AppUser, string> Manager { get; set; }
      
      /// <summary>
      ///     Validates a user before saving
      /// </summary>
      /// <param name="item"></param>
      /// <returns></returns>
      public virtual async Task<IdentityResult> ValidateAsync(AppUser item)
      {
          if (item == null)
          {
              throw new ArgumentNullException("item");
          }
      
          var errors = new List<string>();
      
          ValidateUserName(item, errors);
          await ValidateEmailAsync(item, errors);
      
          if (errors.Count > 0)
          {
              return IdentityResult.Failed(errors.ToArray());
          }
          return IdentityResult.Success;
      }
      
      private void ValidateUserName(AppUser user, List<string> errors)
      {
          if (string.IsNullOrWhiteSpace(user.UserName))
          {
              errors.Add("Name cannot be null or empty.");
          }
          else if (!Regex.IsMatch(user.UserName, @"^[A-Za-z0-9@_\.]+$"))
          {
              // If any characters are not letters or digits, its an illegal user name
              errors.Add(string.Format("User name {0} is invalid, can only contain letters or digits.", user.UserName));
          }
      }
      
      // make sure email is not empty, valid, and unique
      private async Task ValidateEmailAsync(AppUser user, List<string> errors)
      {
          var email = user.Email;
      
          if (string.IsNullOrWhiteSpace(email))
          {
              errors.Add(string.Format("{0} cannot be null or empty.", "Email"));
              return;
          }
          try
          {
              var m = new MailAddress(email);
          }
          catch (FormatException)
          {
              errors.Add(string.Format("Email '{0}' is invalid", email));
              return;
          }
          var owner = await Manager.FindByEmailAsync(email);
          if (owner != null && !owner.Id.Equals(user.Id))
          {
              errors.Add(string.Format(CultureInfo.CurrentCulture, "Email '{0}' is already taken.", email));
          }
      }
      }
      
      public class AppUserManager : UserManager<AppUser>
      {
      public AppUserManager(
          IUserStore<AppUser> store,
          IDataProtectionProvider dataProtectionProvider,
          IIdentityMessageService emailService)
          : base(store)
      {
      
          // Configure validation logic for usernames
          UserValidator = new AppUserValidator(this);
      
    3. And last step is change AppSignInManager. Because now our user names is not unique we use email to log in:

      public class AppSignInManager : SignInManager<AppUser, string>
      {
       ....
      public virtual async Task<SignInStatus> PasswordSignInViaEmailAsync(string userEmail, string password, bool isPersistent, bool shouldLockout)
      {
          var userManager = ((AppUserManager) UserManager);
          if (userManager == null)
          {
              return SignInStatus.Failure;
          }
      
          var user = await UserManager.FindByEmailAsync(userEmail);
          if (user == null)
          {
              return SignInStatus.Failure;
          }
      
          if (await UserManager.IsLockedOutAsync(user.Id))
          {
              return SignInStatus.LockedOut;
          }
      
          if (await UserManager.CheckPasswordAsync(user, password))
          {
              await UserManager.ResetAccessFailedCountAsync(user.Id);
              await SignInAsync(user, isPersistent, false);
              return SignInStatus.Success;
          }
      
          if (shouldLockout)
          {
              // If lockout is requested, increment access failed count which might lock out the user
              await UserManager.AccessFailedAsync(user.Id);
              if (await UserManager.IsLockedOutAsync(user.Id))
              {
                  return SignInStatus.LockedOut;
              }
          }
          return SignInStatus.Failure;
      }
      

      And now code looks like:

      [HttpPost]
      [AllowAnonymous]
      [ValidateAntiForgeryToken]
      public async Task<ActionResult> Index(User model, string returnUrl)
      {
          if (!ModelState.IsValid)
          {
              return View(model);
          }
          var result = 
              await signInManager.PasswordSignInViaEmailAsync(
                  model.Email,
                  model.Password, 
                  model.StaySignedIn,
                  true);
      
          var errorMessage = string.Empty;
          switch (result)
          {
              case SignInStatus.Success:
                  if (IsLocalValidUrl(returnUrl))
                  {
                      return Redirect(returnUrl);
                  }
      
                  return RedirectToAction("Index", "Home");
              case SignInStatus.Failure:
                  errorMessage = Messages.LoginController_Index_AuthorizationError;
                  break;
              case SignInStatus.LockedOut:
                  errorMessage = Messages.LoginController_Index_LockoutError;
                  break;
              case SignInStatus.RequiresVerification:
                  throw new NotImplementedException();
          }
      
          ModelState.AddModelError(string.Empty, errorMessage);
          return View(model);
      }
      

    P.S. I don't really like how I override ValidateEntity method. But I decided to do this because instead I have to implement DbContext class almost identical to IdentityDbContext, thus I have to track changes on it when update identity framework package in my project.

    0 讨论(0)
  • 2020-12-15 12:43

    1st of all i understand the idea behind your thoughts, and as such i'll start to explain the "why" are you not able to create multiple users with the same name.

    The username with the same name: The problem you encounter right now is related to the IdentityDbContext. As you can see (https://aspnetidentity.codeplex.com/SourceControl/latest#src/Microsoft.AspNet.Identity.EntityFramework/IdentityDbContext.cs), the identityDbContext sets up rules about the unique users and roles, First on model creation:

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            if (modelBuilder == null)
            {
                throw new ArgumentNullException("modelBuilder");
            }
    
            // Needed to ensure subclasses share the same table
            var user = modelBuilder.Entity<TUser>()
                .ToTable("AspNetUsers");
            user.HasMany(u => u.Roles).WithRequired().HasForeignKey(ur => ur.UserId);
            user.HasMany(u => u.Claims).WithRequired().HasForeignKey(uc => uc.UserId);
            user.HasMany(u => u.Logins).WithRequired().HasForeignKey(ul => ul.UserId);
            user.Property(u => u.UserName)
                .IsRequired()
                .HasMaxLength(256)
                .HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute("UserNameIndex") { IsUnique = true }));
    
            // CONSIDER: u.Email is Required if set on options?
            user.Property(u => u.Email).HasMaxLength(256);
    
            modelBuilder.Entity<TUserRole>()
                .HasKey(r => new { r.UserId, r.RoleId })
                .ToTable("AspNetUserRoles");
    
            modelBuilder.Entity<TUserLogin>()
                .HasKey(l => new { l.LoginProvider, l.ProviderKey, l.UserId })
                .ToTable("AspNetUserLogins");
    
            modelBuilder.Entity<TUserClaim>()
                .ToTable("AspNetUserClaims");
    
            var role = modelBuilder.Entity<TRole>()
                .ToTable("AspNetRoles");
            role.Property(r => r.Name)
                .IsRequired()
                .HasMaxLength(256)
                .HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute("RoleNameIndex") { IsUnique = true }));
            role.HasMany(r => r.Users).WithRequired().HasForeignKey(ur => ur.RoleId);
        }
    

    secondly on validate entity:

    protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry,
            IDictionary<object, object> items)
        {
            if (entityEntry != null && entityEntry.State == EntityState.Added)
            {
                var errors = new List<DbValidationError>();
                var user = entityEntry.Entity as TUser;
                //check for uniqueness of user name and email
                if (user != null)
                {
                    if (Users.Any(u => String.Equals(u.UserName, user.UserName)))
                    {
                        errors.Add(new DbValidationError("User",
                            String.Format(CultureInfo.CurrentCulture, IdentityResources.DuplicateUserName, user.UserName)));
                    }
                    if (RequireUniqueEmail && Users.Any(u => String.Equals(u.Email, user.Email)))
                    {
                        errors.Add(new DbValidationError("User",
                            String.Format(CultureInfo.CurrentCulture, IdentityResources.DuplicateEmail, user.Email)));
                    }
                }
                else
                {
                    var role = entityEntry.Entity as TRole;
                    //check for uniqueness of role name
                    if (role != null && Roles.Any(r => String.Equals(r.Name, role.Name)))
                    {
                        errors.Add(new DbValidationError("Role",
                            String.Format(CultureInfo.CurrentCulture, IdentityResources.RoleAlreadyExists, role.Name)));
                    }
                }
                if (errors.Any())
                {
                    return new DbEntityValidationResult(entityEntry, errors);
                }
            }
            return base.ValidateEntity(entityEntry, items);
        }
    }
    

    The tip: What you can do to overcome this problem easilly, is, on the ApplicationDbContext that you currently have, override both these methods to overcome this validation

    Warning Without that validation you now can use multiple users with the same name, but you have to implement rules that stop you from creating users in the same customer, with the same username. What you can do is, adding that to the validation.

    Hope the help was valuable :) Cheers!

    0 讨论(0)
提交回复
热议问题