2017-08-25 54 views
2

我有幾個遺留的ASP.NET Web應用程序共享一個數據庫的ASP.NET成員資格。我想轉向使用.NET Core和IdentityServer4的微服務體系結構,並讓新微服務生態系統中的身份服務器使用現有的ASP.NET Membership用戶存儲,但.NET Core似乎不支持ASP.NET Membership所有。如何在ASP.NET身份中使用ASP.NET成員資格數據庫?

我目前有一個概念驗證站起來涉及一個Web API,身份服務器和MVC網絡應用程序作爲我的客戶端。身份服務器實現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<Startup>(); 
     } 

     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<IdentityOptions>(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<StoreContext>(options => 
      options.UseSqlServer(Configuration.GetConnectionString("MyConnectionString"))); 

     /* Add ASP.NET Identity to use for registration and authentication. */ 
     services.AddIdentity<AspNetMembershipUser, IdentityRole>() 
      .AddEntityFrameworkStores<StoreContext>() 
      .AddUserStore<AspNetMembershipUserStore>() 
      .AddDefaultTokenProviders(); 

     services.AddTransient<IPasswordHasher<AspNetMembershipUser>, 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<AspNetMembershipUser>, IUserPasswordStore<AspNetMembershipUser>, IUserEmailStore<AspNetMembershipUser> 
{ 
    private readonly StoreContext _dbcontext; 

    public AspNetMembershipUserStore(StoreContext dbContext) 
    { 
     _dbcontext = dbContext; 
    } 

    public Task<IdentityResult> 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<IdentityResult> 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<AspNetMembershipUser> 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<AspNetMembershipUser> 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<AspNetMembershipUser> 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<string> GetEmailAsync(AspNetMembershipUser user, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => user.Email); 
    } 

    public Task<bool> GetEmailConfirmedAsync(AspNetMembershipUser user, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => user.EmailConfirmed); 
    } 

    public Task<string> GetNormalizedEmailAsync(AspNetMembershipUser user, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => user.NormalizedEmail); 
    } 

    public Task<string> GetNormalizedUserNameAsync(AspNetMembershipUser user, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => user.NormalizedUserName); 
    } 

    public Task<string> GetPasswordHashAsync(AspNetMembershipUser user, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => user.PasswordHash); 
    } 

    public Task<string> GetUserIdAsync(AspNetMembershipUser user, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => user.Id.ToString()); 
    } 

    public Task<string> GetUserNameAsync(AspNetMembershipUser user, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => user.UserName); 
    } 

    public Task<bool> 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<IdentityResult> 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<string> 
      { 
       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<AspNetMembershipUser> 
{ 
    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); 
     } 
    } 
} 

回答

1

我的一個同事能夠幫助我。以下是散列函數的樣子。通過這一更改,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); 
    } 

你可以找到所有的source code on GitHib

相關問題