如何在ASP.NET标识中使用ASP.NET成员资格数据库?

我有几个遗留的ASP.NET Web应用程序共享ASP.NET成员资格的数据库。 我想转向使用.NET Core和IdentityServer4的微服务架构,并在新的微服务生态系统中使用身份服务器来使用现有的ASP.NET Membership用户存储,但.NET Core似乎不支持ASP.NET Membership所有。

我目前有一个概念证据,涉及Web API,身份服务器和MVC Web应用程序作为我的客户端。 身份服务器实现IdentityUser的子类,并实现IUserStore / IUserPasswordStore / IUserEmailStore以使其适应我现有数据库中的ASP.NET Membership表。 我可以注册新用户并通过我的POC MVC客户端应用程序登录,但这些用户无法登录我的旧应用程序。 相反,在旧版应用程序中注册的用户无法登录我的POC MVC客户端。 我假设它是因为我的IPasswordHasher实现并没有像我的遗留应用程序中的ASP.NET成员资格一样散列密码。

以下是我的代码。 任何洞察我可能做错的事情都将非常感激。 安全和加密不是我的强项。

Startup.cs

public class Startup { public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); if (env.IsDevelopment()) { // For more details on using the user secret store see https://go.microsoft.com/fwlink/?LinkID=532709 builder.AddUserSecrets(); } builder.AddEnvironmentVariables(); Configuration = builder.Build(); } public IConfigurationRoot Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { /* Add CORS policy */ services.AddCors(options => { // this defines a CORS policy called "default" options.AddPolicy("default", policy => { policy.WithOrigins("http://localhost:5003") .AllowAnyHeader() .AllowAnyMethod(); }); }); services.AddMvcCore() .AddAuthorization() .AddJsonFormatters(); /* Add MVC componenets. */ services.AddMvc(); /* Configure IdentityServer. */ services.Configure(options => { // Password settings options.Password.RequireDigit = true; options.Password.RequiredLength = 8; options.Password.RequireNonAlphanumeric = false; options.Password.RequireUppercase = true; options.Password.RequireLowercase = false; // Lockout settings options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30); options.Lockout.MaxFailedAccessAttempts = 10; // Cookie settings options.Cookies.ApplicationCookie.ExpireTimeSpan = TimeSpan.FromDays(150); options.Cookies.ApplicationCookie.LoginPath = "/Account/Login"; options.Cookies.ApplicationCookie.LogoutPath = "/Account/Logout"; // User settings options.User.RequireUniqueEmail = true; }); /* Add the DbContext */ services.AddDbContext(options => options.UseSqlServer(Configuration.GetConnectionString("MyConnectionString"))); /* Add ASP.NET Identity to use for registration and authentication. */ services.AddIdentity() .AddEntityFrameworkStores() .AddUserStore() .AddDefaultTokenProviders(); services.AddTransient<IPasswordHasher, AspNetMembershipPasswordHasher>(); /* Add IdentityServer and its components. */ services.AddIdentityServer() .AddInMemoryCaching() .AddTemporarySigningCredential() .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryIdentityResources(Config.GetIdentityResources()) .AddInMemoryClients(Config.GetClients()); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { /* Configure logging. */ loggerFactory.AddConsole(Configuration.GetSection("Logging")); if (env.IsDevelopment()) { loggerFactory.AddDebug(); app.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); app.UseBrowserLink(); } else { app.UseExceptionHandler("/Home/Error"); } /* Configure wwwroot */ app.UseStaticFiles(); /* Configure CORS */ app.UseCors("default"); /* Configure AspNet Identity */ app.UseIdentity(); /* Configure IdentityServer */ app.UseIdentityServer(); /* Configure MVC */ app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); } } 

AspNetMembershipUser.cs

 public class AspNetMembershipUser : IdentityUser { public string PasswordSalt { get; set; } public int PasswordFormat { get; set; } } 

AspNetMembershipUserStore.cs

 public class AspNetMembershipUserStore : IUserStore, IUserPasswordStore, IUserEmailStore { private readonly StoreContext _dbcontext; public AspNetMembershipUserStore(StoreContext dbContext) { _dbcontext = dbContext; } public Task CreateAsync(AspNetMembershipUser user, CancellationToken cancellationToken) { return Task.Factory.StartNew(() => { try { User dbUser = new User(); this.Convert(user, dbUser); _dbcontext.Users.Add(dbUser); _dbcontext.SaveChanges(); return IdentityResult.Success; } catch (Exception ex) { return IdentityResult.Failed(new IdentityError { Code = ex.GetType().Name, Description = ex.Message }); } }); } public Task DeleteAsync(AspNetMembershipUser user, CancellationToken cancellationToken) { return Task.Factory.StartNew(() => { try { User dbUser = _dbcontext.Users .Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership) .Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetApplication) .Include(u => u.UserGroups) .SingleOrDefault(u => u.ProviderUserName == user.NormalizedUserName); if (dbUser != null) { _dbcontext.AspNetUsers.Remove(dbUser.AspNetUser); _dbcontext.Users.Remove(dbUser); _dbcontext.SaveChanges(); } return IdentityResult.Success; } catch (Exception ex) { return IdentityResult.Failed(new IdentityError { Code = ex.GetType().Name, Description = ex.Message }); } }); } public void Dispose() { _dbcontext.Dispose(); } public Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken) { return Task.Factory.StartNew(() => { User dbUser = _dbcontext.Users .Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership) .Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetApplication) .Include(u => u.UserGroups) .SingleOrDefault(u => u.ProviderEmailAddress == normalizedEmail); if (dbUser == null) { return null; } AspNetMembershipUser user = new AspNetMembershipUser(); this.Convert(dbUser, user); return user; }); } public Task FindByIdAsync(string userId, CancellationToken cancellationToken) { long lUserId = long.Parse(userId); return Task.Factory.StartNew(() => { User dbUser = _dbcontext.Users .Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership) .Include(u => u.AspNetUsers).ThenInclude(u=> u.AspNetApplication) .Include(u => u.UserGroups) .SingleOrDefault(u => u.UserId == lUserId); if (dbUser == null) { return null; } AspNetMembershipUser user = new AspNetMembershipUser(); this.Convert(dbUser, user); return user; }); } public Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) { return Task.Factory.StartNew(() => { User dbUser = _dbcontext.Users .Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership) .Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetApplication) .Include(u => u.UserGroups) .SingleOrDefault(u => u.ProviderUserName == normalizedUserName); if (dbUser == null) { return null; } AspNetMembershipUser user = new AspNetMembershipUser(); this.Convert(dbUser, user); return user; }); } public Task GetEmailAsync(AspNetMembershipUser user, CancellationToken cancellationToken) { return Task.Factory.StartNew(() => user.Email); } public Task GetEmailConfirmedAsync(AspNetMembershipUser user, CancellationToken cancellationToken) { return Task.Factory.StartNew(() => user.EmailConfirmed); } public Task GetNormalizedEmailAsync(AspNetMembershipUser user, CancellationToken cancellationToken) { return Task.Factory.StartNew(() => user.NormalizedEmail); } public Task GetNormalizedUserNameAsync(AspNetMembershipUser user, CancellationToken cancellationToken) { return Task.Factory.StartNew(() => user.NormalizedUserName); } public Task GetPasswordHashAsync(AspNetMembershipUser user, CancellationToken cancellationToken) { return Task.Factory.StartNew(() => user.PasswordHash); } public Task GetUserIdAsync(AspNetMembershipUser user, CancellationToken cancellationToken) { return Task.Factory.StartNew(() => user.Id.ToString()); } public Task GetUserNameAsync(AspNetMembershipUser user, CancellationToken cancellationToken) { return Task.Factory.StartNew(() => user.UserName); } public Task HasPasswordAsync(AspNetMembershipUser user, CancellationToken cancellationToken) { return Task.Factory.StartNew(() => !string.IsNullOrEmpty(user.PasswordHash)); } public Task SetEmailAsync(AspNetMembershipUser user, string email, CancellationToken cancellationToken) { return Task.Factory.StartNew(() => user.Email = email); } public Task SetEmailConfirmedAsync(AspNetMembershipUser user, bool confirmed, CancellationToken cancellationToken) { return Task.Factory.StartNew(() => user.EmailConfirmed = confirmed); } public Task SetNormalizedEmailAsync(AspNetMembershipUser user, string normalizedEmail, CancellationToken cancellationToken) { return Task.Factory.StartNew(() => user.NormalizedEmail = normalizedEmail); } public Task SetNormalizedUserNameAsync(AspNetMembershipUser user, string normalizedName, CancellationToken cancellationToken) { return Task.Factory.StartNew(() => user.NormalizedUserName = normalizedName); } public Task SetPasswordHashAsync(AspNetMembershipUser user, string passwordHash, CancellationToken cancellationToken) { return Task.Factory.StartNew(() => user.PasswordHash = passwordHash); } public Task SetUserNameAsync(AspNetMembershipUser user, string userName, CancellationToken cancellationToken) { return Task.Factory.StartNew(() => user.UserName = userName); } public Task UpdateAsync(AspNetMembershipUser user, CancellationToken cancellationToken) { return Task.Factory.StartNew(() => { try { User dbUser = _dbcontext.Users .Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership) .Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetApplication) .Include(u => u.UserGroups) .SingleOrDefault(u => u.UserId.ToString() == user.Id); if (dbUser != null) { this.Convert(user, dbUser); _dbcontext.Users.Update(dbUser); _dbcontext.SaveChanges(); } return IdentityResult.Success; } catch(Exception ex) { return IdentityResult.Failed(new IdentityError { Code = ex.GetType().Name, Description = ex.Message }); } }); } private void Convert(User from, AspNetMembershipUser to) { to.Id = from.ProviderUserKey.ToString(); to.UserName = from.ProviderUserName; to.NormalizedUserName = from.ProviderUserName.ToLower(); to.Email = from.ProviderEmailAddress; to.NormalizedEmail = from.ProviderEmailAddress.ToLower(); to.EmailConfirmed = true; to.PasswordHash = from.AspNetUser.AspNetMembership.Password; to.PasswordSalt = from.AspNetUser.AspNetMembership.PasswordSalt; to.PasswordFormat = from.AspNetUser.AspNetMembership.PasswordFormat; to.AccessFailedCount = from.AspNetUser.AspNetMembership.FailedPasswordAttemptCount; to.EmailConfirmed = true; to.Roles.Clear(); from.UserGroups.ToList().ForEach(ug => { to.Roles.Add(new IdentityUserRole { RoleId = ug.GroupId.ToString(), UserId = ug.UserId.ToString() }); }); to.PhoneNumber = from.Phone ?? from.ShippingPhone; to.PhoneNumberConfirmed = !string.IsNullOrEmpty(to.PhoneNumber); to.SecurityStamp = from.AspNetUser.AspNetMembership.PasswordSalt; } private void Convert(AspNetMembershipUser from , User to) { AspNetApplication application = _dbcontext.AspNetApplications.First(); to.ProviderUserKey = Guid.Parse(from.Id); to.ProviderUserName = from.UserName; to.ProviderEmailAddress = from.Email; to.InternalEmail = $"c_{Guid.NewGuid().ToString()}@mycompany.com"; to.AccountOwner = "MYCOMPANY"; to.UserStatusId = (int)UserStatus.Normal; AspNetUser aspNetUser = to.AspNetUser; if (to.AspNetUser == null) { to.AspNetUser = new AspNetUser { ApplicationId = application.ApplicationId, AspNetApplication= application, AspNetMembership = new AspNetMembership { ApplicationId = application.ApplicationId, AspNetApplication = application } }; } to.AspNetUser.UserId = Guid.Parse(from.Id); to.AspNetUser.UserName = from.UserName; to.AspNetUser.LoweredUserName = from.UserName.ToLower(); to.AspNetUser.LastActivityDate = DateTime.UtcNow; to.AspNetUser.IsAnonymous = false; to.AspNetUser.ApplicationId = application.ApplicationId; to.AspNetUser.AspNetMembership.CreateDate = DateTime.UtcNow; to.AspNetUser.AspNetMembership.Email = from.Email; to.AspNetUser.AspNetMembership.IsApproved = true; to.AspNetUser.AspNetMembership.LastLoginDate = DateTime.Parse("1754-01-01 00:00:00.000"); to.AspNetUser.AspNetMembership.LastLockoutDate = DateTime.Parse("1754-01-01 00:00:00.000"); to.AspNetUser.AspNetMembership.LastPasswordChangedDate = DateTime.Parse("1754-01-01 00:00:00.000"); to.AspNetUser.AspNetMembership.LoweredEmail = from.NormalizedEmail.ToLower(); to.AspNetUser.AspNetMembership.Password = from.PasswordHash; to.AspNetUser.AspNetMembership.PasswordSalt = from.PasswordSalt; to.AspNetUser.AspNetMembership.PasswordFormat = from.PasswordFormat; to.AspNetUser.AspNetMembership.IsLockedOut = false; to.AspNetUser.AspNetMembership.FailedPasswordAnswerAttemptWindowStart = DateTime.Parse("1754-01-01 00:00:00.000"); to.AspNetUser.AspNetMembership.FailedPasswordAttemptWindowStart = DateTime.Parse("1754-01-01 00:00:00.000"); // Merge Groups/Roles to.UserGroups .Where(ug => !from.Roles.Any(r => ug.GroupId.ToString() == r.RoleId)) .ToList() .ForEach(ug => to.UserGroups.Remove(ug)); to.UserGroups .Join(from.Roles, ug => ug.GroupId.ToString(), r => r.RoleId, (ug, r) => new { To = ug, From = r }) .ToList() .ForEach(j => { j.To.UserId = long.Parse(j.From.UserId); j.To.GroupId = int.Parse(j.From.RoleId); }); from.Roles .Where(r => !to.UserGroups.Any(ug => ug.GroupId.ToString() == r.RoleId)) .ToList() .ForEach(r => { to.UserGroups.Add(new UserGroup { UserId = long.Parse(from.Id), GroupId = int.Parse(r.RoleId) }); }); } } 

AspNetMembershipPasswordHasher.cs

 public class AspNetMembershipPasswordHasher : IPasswordHasher { private readonly int _saltSize; private readonly int _bytesRequired; private readonly int _iterations; public AspNetMembershipPasswordHasher() { this._saltSize = 128 / 8; this._bytesRequired = 32; this._iterations = 1000; } public string HashPassword(AspNetMembershipUser user, string password) { string passwordHash = null; string passwordSalt = null; this.HashPassword(password, out passwordHash, ref passwordSalt); user.PasswordSalt = passwordSalt; return passwordHash; } public PasswordVerificationResult VerifyHashedPassword(AspNetMembershipUser user, string hashedPassword, string providedPassword) { // Throw an error if any of our passwords are null if (hashedPassword == null) { throw new ArgumentNullException("hashedPassword"); } if (providedPassword == null) { throw new ArgumentNullException("providedPassword"); } string providedPasswordHash = null; if (user.PasswordFormat == 0) { providedPasswordHash = providedPassword; } else if (user.PasswordFormat == 1) { string providedPasswordSalt = user.PasswordSalt; this.HashPassword(providedPassword, out providedPasswordHash, ref providedPasswordSalt); } else { throw new NotSupportedException("Encrypted passwords are not supported."); } if (providedPasswordHash == hashedPassword) { return PasswordVerificationResult.Success; } else { return PasswordVerificationResult.Failed; } } private void HashPassword(string password, out string passwordHash, ref string passwordSalt) { byte[] hashBytes = null; byte[] saltBytes = null; byte[] totalBytes = new byte[this._saltSize + this._bytesRequired]; if (!string.IsNullOrEmpty(passwordSalt)) { // Using existing salt. using (var pbkdf2 = new Rfc2898DeriveBytes(password, Convert.FromBase64String(passwordSalt), this._iterations)) { saltBytes = pbkdf2.Salt; hashBytes = pbkdf2.GetBytes(this._bytesRequired); } } else { // Generate a new salt. using (var pbkdf2 = new Rfc2898DeriveBytes(password, this._saltSize, this._iterations)) { saltBytes = pbkdf2.Salt; hashBytes = pbkdf2.GetBytes(this._bytesRequired); } } Buffer.BlockCopy(saltBytes, 0, totalBytes, 0, this._saltSize); Buffer.BlockCopy(hashBytes, 0, totalBytes, this._saltSize, this._bytesRequired); using (SHA256 hashAlgorithm = SHA256.Create()) { passwordHash = Convert.ToBase64String(hashAlgorithm.ComputeHash(totalBytes)); passwordSalt = Convert.ToBase64String(saltBytes); } } } 

我的一个同事能够帮助我。 下面是哈希函数应该是什么样子。 通过此更改,ASP.NET Identity可以捎带现有的ASP.NET成员资格数据库。

 private void HashPassword(string password, out string passwordHash, ref string passwordSalt) { byte[] passwordBytes = Encoding.Unicode.GetBytes(password); byte[] saltBytes = null; if (!string.IsNullOrEmpty(passwordSalt)) { saltBytes = Convert.FromBase64String(passwordSalt); } else { saltBytes = new byte[128 / 8]; using (var rng = RandomNumberGenerator.Create()) { rng.GetBytes(saltBytes); } } byte[] totalBytes = new byte[saltBytes.Length + passwordBytes.Length]; Buffer.BlockCopy(saltBytes, 0, totalBytes, 0, saltBytes.Length); Buffer.BlockCopy(passwordBytes, 0, totalBytes, saltBytes.Length, passwordBytes.Length); using (SHA1 hashAlgorithm = SHA1.Create()) { passwordHash = Convert.ToBase64String(hashAlgorithm.ComputeHash(totalBytes)); } passwordSalt = Convert.ToBase64String(saltBytes); } 

您可以在GitHib上找到所有源代码 。