C# Microsoft ASP.NET标识-多个同名用户
我正在尝试一些我认为非常奇特的东西,我面临一些问题,我希望在StackOverflow上的用户的帮助下可以解决这些问题 故事 我正在写需要认证和注册的申请书。我选择使用C# Microsoft ASP.NET标识-多个同名用户,c#,asp.net,authentication,forms-authentication,asp.net-identity,C#,Asp.net,Authentication,Forms Authentication,Asp.net Identity,我正在尝试一些我认为非常奇特的东西,我面临一些问题,我希望在StackOverflow上的用户的帮助下可以解决这些问题 故事 我正在写需要认证和注册的申请书。我选择使用Microsoft.AspNet.Identity。我过去没有经常使用它,所以不要在这个决定上评判我 上面提到的框架包含一个users表,其中包含所有注册用户 我创建了一个示例图片来展示应用程序将如何工作 该应用程序由3个不同的组件组成: 后端(WebAPI) 客户(直接使用WebAPI) 最终用户(使用移动应用程序-iOS)
Microsoft.AspNet.Identity
。我过去没有经常使用它,所以不要在这个决定上评判我
上面提到的框架包含一个users表,其中包含所有注册用户
我创建了一个示例图片来展示应用程序将如何工作
该应用程序由3个不同的组件组成:
User 1
是Customer 1
的客户,但也可能是Customer 2
的客户
现在,客户可以邀请会员使用移动应用程序。当客户这样做时,最终用户确实会收到一封电子邮件,其中包含指向活动用户本人的链接
现在,只要您的用户是唯一的,这一切都可以正常工作,但我确实有一个用户是Customer 1
和Customer 2
的客户。两个客户都可以邀请同一个用户,用户需要注册2次,每个客户注册一次
问题
在Microsoft.AspNet.Identity
框架中,用户应该是唯一的,根据我的情况,我无法管理
问题
是否可以向IdentityUser
添加额外的参数,以确保用户是唯一的
我已经做了什么
创建从IdentityUser
继承的自定义类,该类包含应用程序id:
public class AppServerUser : IdentityUser
{
#region Properties
/// <summary>
/// Gets or sets the id of the member that this user belongs to.
/// </summary>
public int MemberId { get; set; }
#endregion
}
使用框架的已修改调用
IUserStore<IdentityUser> -> IUserStore<AppServerUser>
UserManager<IdentityUser>(_userStore) -> UserManager<AppServerUser>(_userStore);
我相信这是一个解决办法
我确实认为我需要更改UserManager
,但我不确定。
我希望这里有人对这个框架有足够的知识来帮助我,因为它确实阻碍了我们的应用程序开发
如果这是不可能的,我也想知道,也许你可以告诉我另一个框架,允许我这样做
注意:我不想自己写一个完整的用户管理,因为这将是对轮子的重新发明。首先,我理解你想法背后的想法,因此我将开始解释“为什么”你不能创建多个同名用户
同名用户名:
您现在遇到的问题与IdentityDbContext有关。如您所见(),identityDbContext设置了关于唯一用户和角色的规则,首先是在模型创建时:
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);
}
模型创建时受保护的覆盖无效(DbModelBuilder modelBuilder)
{
if(modelBuilder==null)
{
抛出新的ArgumentNullException(“modelBuilder”);
}
//需要确保子类共享同一个表
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(“索引”,新索引注释(新索引属性(“用户名索引”){IsUnique=true}));
//考虑:如果设置了选项,则需要发送电子邮件?
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”);
属性(r=>r.Name)
.IsRequired()
.HasMaxLength(256)
.HasColumnAnnotation(“索引”,新索引注释(新索引属性(“RoleNameIndex”){IsUnique=true});
role.HasMany(r=>r.Users).WithRequired().HasForeignKey(ur=>ur.RoleId);
}
第二,关于验证实体:
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);
}
}
受保护的覆盖DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry,
(词典项目)
{
if(entityEntry!=null&&entityEntry.State==EntityState.Added)
{
var errors=新列表();
var user=entityEntry.Entity作为TUser;
//检查用户名和电子邮件的唯一性
如果(用户!=null)
{
if(Users.Any(u=>String.Equals(u.UserName,user.UserName)))
{
添加(新的DbValidationError(“用户”,
格式(CultureInfo.CurrentCulture,IdentityResources.DuplicateUserName,user.UserName));
}
if(RequireUniqueEmail&&Users.Any(u=>String.Equals(u.Email,user.Email)))
{
添加(新的DbValidationError(“用户”,
格式(CultureInfo.CurrentCulture、IdentityResources.DuplicateEmail、user.Email));
}
}
其他的
{
var role=entityEntry.Entity作为TRole;
//检查角色名称的唯一性
if(role!=null&&Roles.Any(r=>String.Equals(r.Name,role.Name)))
{
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);
}
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);
}
}
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;
}
}
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);
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;
}
[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);
}