Identité Microsoft ASP.NET – Plusieurs utilisateurs portant le même nom

J’essaie quelque chose d’assez exotique, je crois, et je suis confronté à quelques problèmes, qui, j’espère, peuvent être résolus avec l’aide des utilisateurs ici sur StackOverflow.

L’histoire

J’écris et demande ce qui nécessite une authentification et un enregistrement. J’ai choisi d’utiliser Microsoft.AspNet.Identity . Je ne l’ai pas beaucoup utilisé par le passé, alors ne me jugez pas sur cette décision.

Le cadre mentionné ci-dessus contient une table d’utilisateurs, qui contient tous les utilisateurs enregistrés.

J’ai créé un exemple de photo pour montrer le fonctionnement de l’application.

entrez la description de l'image ici

L’application se compose de 3 composants différents:

  1. Backend (WebAPI).
  2. Clients (utilisation directe de WebAPI).
  3. Utilisateurs finaux (Utilisation d’une application mobile – iOS).

J’ai donc un backend sur lequel les clients peuvent s’inscrire. Il y a un utilisateur unique par membre, donc pas de problème ici. Un client est une société ici et les utilisateurs finaux sont des clients de la société.

Vous voyez peut-être déjà le problème. Il est parfaitement possible que l’ User 1 soit un client du Customer 1 mais également du Customer 2 .

Désormais, un client peut inviter un membre à utiliser l’application Mobile. Lorsqu’un client fait cela, l’utilisateur final reçoit un courrier électronique avec un lien pour l’activer lui-même.

Maintenant, tout fonctionne bien tant que vos utilisateurs sont uniques, mais j’ai un utilisateur qui est un client de Customer 1 et Customer 2 . Les deux clients peuvent inviter le même utilisateur et celui-ci devra s’inscrire deux fois, une pour chaque Customer .

Le problème

Dans Microsoft.AspNet.Identity cadre Microsoft.AspNet.Identity , les utilisateurs doivent être uniques, ce que, selon ma situation, je ne suis pas en mesure de gérer.

La question

Est-il possible d’append à IdentityUser des parameters supplémentaires IdentityUser qu’un utilisateur est unique?

Ce que j’ai déjà fait

  1. Créez une classe personnalisée qui hérite d’ IdentityUser et qui inclut un ID d’application:

     public class AppServerUser : IdentityUser { #region Properties ///  /// Gets or sets the id of the member that this user belongs to. ///  public int MemberId { get; set; } #endregion } 
  2. Modifié mon IDbContext conséquence:

     public class AppServerContext : IdentityDbContext, IDbContext { } 
  3. Appels modifiés utilisant le framework.

     IUserStore -> IUserStore UserManager(_userStore) -> UserManager(_userStore); 

    _userStore est hors du type IUserStore

Cependant, lorsque j’enregistre un utilisateur avec un nom d’utilisateur déjà utilisé, je reçois quand même un message d’erreur indiquant que le nom d’utilisateur est déjà utilisé:

 var result = await userManager.CreateAsync(new AppServerUser {UserName = "testing"}, "testing"); 

Ce que je crois être une solution

Je crois que je dois changer le UserManager mais je n’en suis pas sûr. J’espère que quelqu’un ici a suffisamment de connaissances sur le cadre pour m’aider, car il bloque vraiment le développement de nos applications.

Si ce n’est pas possible, je voudrais également savoir, et vous pouvez peut-être me diriger vers un autre cadre qui me permet de le faire.

Remarque: je ne veux pas écrire moi-même toute une gestion d’utilisateurs, car cela va réinventer la roue.

Tout d’abord, je comprends l’idée qui sous-tend vos pensées et, à ce titre, je vais commencer à expliquer le «pourquoi» n’est-il pas possible de créer plusieurs utilisateurs avec le même nom.

Le nom d’utilisateur portant le même nom: Le problème rencontré actuellement est lié à IdentityDbContext. Comme vous pouvez le voir ( https://aspnetidentity.codeplex.com/SourceControl/latest#src/Microsoft.AspNet.Identity.EntityFramework/IdentityDbContext.cs ), identityDbContext définit les règles relatives aux utilisateurs et rôles uniques, Premier sur la création de modèle :

 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() .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 IndexAtsortingbute("UserNameIndex") { IsUnique = true })); // CONSIDER: u.Email is Required if set on options? user.Property(u => u.Email).HasMaxLength(256); modelBuilder.Entity() .HasKey(r => new { r.UserId, r.RoleId }) .ToTable("AspNetUserRoles"); modelBuilder.Entity() .HasKey(l => new { l.LoginProvider, l.ProviderKey, l.UserId }) .ToTable("AspNetUserLogins"); modelBuilder.Entity() .ToTable("AspNetUserClaims"); var role = modelBuilder.Entity() .ToTable("AspNetRoles"); role.Property(r => r.Name) .IsRequired() .HasMaxLength(256) .HasColumnAnnotation("Index", new IndexAnnotation(new IndexAtsortingbute("RoleNameIndex") { IsUnique = true })); role.HasMany(r => r.Users).WithRequired().HasForeignKey(ur => ur.RoleId); } 

d’autre part sur l’entité validée:

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

Le conseil: pour résoudre ce problème facilement, c’est, sur le champ ApplicationDbContext que vous possédez actuellement, remplacer ces deux méthodes pour surmonter cette validation.

Avertissement Sans cette validation, vous pouvez maintenant utiliser plusieurs utilisateurs avec le même nom, mais vous devez mettre en œuvre des règles qui vous empêchent de créer des utilisateurs dans le même client, avec le même nom d’utilisateur. Ce que vous pouvez faire est d’append cela à la validation.

J’espère que l’aide était précieuse 🙂 À la votre!

Peut-être que quelqu’un peut trouver cela utile. Dans notre projet, nous utilisons l’identité ASP.NET 2 et, un jour, nous avons rencontré le cas où deux utilisateurs ont des noms identiques. Nous utilisons les e-mails comme identifiants dans notre application et ils doivent, en effet, être uniques. Mais nous ne voulons de toute façon pas que les noms d’utilisateurs soient uniques. Ce que nous avons fait vient de personnaliser quelques classes de cadre d’identité comme suit:

  1. Nous avons modifié notre AppIdentityDbContext en créant un index sur le champ Nom d’utilisateur non unique et en remplaçant ValidateEntity de manière délicate. Et ensuite, en utilisant les migrations update database. Le code ressemble à:

     public class AppIdentityDbContext : IdentityDbContext { 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() .Property(c => c.UserName) .HasColumnAnnotation( "Index", new IndexAnnotation( new IndexAtsortingbute("UserNameIndex") { IsUnique = false })); modelBuilder .Entity() .Property(c => c.Email) .IsRequired() .HasColumnAnnotation( "Index", new IndexAnnotation(new[] { new IndexAtsortingbute("EmailIndex") {IsUnique = true} })); } ///  /// Override 'ValidateEntity' to support multiple users with the same name ///  ///  ///  ///  protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary 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. Créez une classe personnalisée de l’ IIdentityValidator et définissez-la sur notre UserManager.UserValidator :

     public class AppUserValidator : IIdentityValidator { ///  /// Constructor ///  ///  public AppUserValidator(UserManager manager) { Manager = manager; } private UserManager Manager { get; set; } ///  /// Validates a user before saving ///  ///  ///  public virtual async Task ValidateAsync(AppUser item) { if (item == null) { throw new ArgumentNullException("item"); } var errors = new List(); 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 errors) { if (ssortingng.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(ssortingng.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 errors) { var email = user.Email; if (ssortingng.IsNullOrWhiteSpace(email)) { errors.Add(ssortingng.Format("{0} cannot be null or empty.", "Email")); return; } try { var m = new MailAddress(email); } catch (FormatException) { errors.Add(ssortingng.Format("Email '{0}' is invalid", email)); return; } var owner = await Manager.FindByEmailAsync(email); if (owner != null && !owner.Id.Equals(user.Id)) { errors.Add(ssortingng.Format(CultureInfo.CurrentCulture, "Email '{0}' is already taken.", email)); } } } public class AppUserManager : UserManager { public AppUserManager( IUserStore store, IDataProtectionProvider dataProtectionProvider, IIdentityMessageService emailService) : base(store) { // Configure validation logic for usernames UserValidator = new AppUserValidator(this); 
  3. Et la dernière étape est de changer AppSignInManager . Parce que nos noms d’utilisateur ne sont pas uniques, nous utilisons un email pour vous connecter:

     public class AppSignInManager : SignInManager { .... public virtual async Task PasswordSignInViaEmailAsync(ssortingng userEmail, ssortingng 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; } 

    Et maintenant, le code ressemble à:

     [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task Index(User model, ssortingng returnUrl) { if (!ModelState.IsValid) { return View(model); } var result = await signInManager.PasswordSignInViaEmailAsync( model.Email, model.Password, model.StaySignedIn, true); var errorMessage = ssortingng.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(ssortingng.Empty, errorMessage); return View(model); } 

PS Je n’aime pas trop la façon dont je substitue la méthode ValidateEntity . Mais j’ai décidé de le faire parce que je devais plutôt implémenter une classe DbContext presque identique à IdentityDbContext , donc je devais suivre les modifications qui y étaient apscopes lors de la mise à jour du package de framework d’identité dans mon projet.