24

Я работаю над проектом ASP.NET MVC 5 в VS2013, .NET 4.5.1, который использует Entity Framework 6 Code-First. У меня есть база данных приличного размера, построенная и несколько работающая (проект составляет около двух недель). Я хочу интегрировать аутентификацию пользователя сейчас, но я не уверен, как подойти к ней. Проведя большую часть дня исследования, я решил дать новой структуре ASP.NET Identity возможность написать собственные поставщики членства или роли. Я запутался в том, как заставить все это работать с существующей базой данных/модели, которые у меня есть.Интеграция идентификатора ASP.NET в существующий DbContext

В настоящее время у меня есть объект, называемый Employee, который содержит основную информацию о сотрудниках (на данный момент). Подумав об этом весь день, я решил отделить аутентификацию от него до объекта User, чего хочет Идентификация. Это было сказано, как я могу заставить все это работать?

Вот мой Employee класс:

public class Employee : Person { 
    public int EmployeeId { get; set; } 
    public byte CompanyId { get; set; } 
    public string Name { 
     get { 
      return String.Format("{0} {1}", this.FirstName, this.LastName); 
     } 
    } 
    public string Password { get; set; } 
    public bool IsActive { get; set; } 

    public virtual ICollection<Address> Addresses { get; set; } 
    public virtual Company Company { get; set; } 
    public virtual ICollection<Email> Emails { get; set; } 
    public virtual ICollection<Phone> Phones { get; set; } 

    public Employee() { 
     this.Addresses = new List<Address>(); 
     this.Emails = new List<Email>(); 
     this.Phones = new List<Phone>(); 
    } 
} 

И мой DbContext производный класс:

public class DatabaseContext : DbContext { 
    static DatabaseContext() { 
     Database.SetInitializer<DatabaseContext>(new DatabaseInitializer()); 
    } 

    public DatabaseContext() 
     : base("Name=DatabaseContext") { 
     this.Database.Initialize(true); 
    } 

    public DatabaseContext(
     string connectionString) 
     : base(connectionString) { 
     this.Database.Initialize(true); 
    } 

    /// DbSets... 

    public override int SaveChanges() { 
     try { 
      return base.SaveChanges(); 
     } catch (DbEntityValidationException e) { 
      IEnumerable<string> errors = e.EntityValidationErrors.SelectMany(
       x => 
        x.ValidationErrors).Select(
       x => 
        String.Format("{0}: {1}", x.PropertyName, x.ErrorMessage)); 

      throw new DbEntityValidationException(String.Join("; ", errors), e.EntityValidationErrors); 
     } 
    } 

    protected override void OnModelCreating(
     DbModelBuilder modelBuilder) { 
     modelBuilder.Ignore<Coordinate>(); 

     /// Configs... 

     base.OnModelCreating(modelBuilder); 
    } 
} 

ответ

14

Так, проведя около дня или поэтому чтение и чтение, я закончил строить свою собственную реализацию Подлинность. Сначала я сделал свой существующий объект Employee и расширил его, чтобы наследовать от IUser<int>. IUser<int> - это интерфейс, который является частью Identity 2.0 (в настоящее время в альфа), который позволяет настроить тип первичного ключа на что-то, отличное от string, по умолчанию в 1.0. Из-за того, как я храню данные, моя реализация была действительно конкретной. Например, Employee может иметь несколько связанных с ним объектов Email, и для моего приложения я хотел использовать электронные письма в качестве имен пользователей. Таким образом, я просто установить UserName свойства возвращаться в Employee «сек работы по электронной почте:

public string UserName { 
    get { 
     if (this.WorkEmail != null) { 
      return this.WorkEmail.Address; 
     } 

     return null; 
    } 
    set { 
     /// This property is non-settable. 
    } 
} 

примечания стороны, так как я не собираюсь использовать сеттер для свойства, есть уборщик способ obsoleting его кроме простого оставления его пустым?

Продолжая, я также добавил PasswordHash. Я добавил свой собственный объект Role, наследующий от IRole<int>. Наконец, объекты Employee и Role имеют ICollection<T>, связывающие друг с другом. Еще одно замечание: реализация Identity Framework Identity вручную создает таблицу сопоставления UserRoles вместо того, чтобы использовать свои собственные возможности настройки, и я не могу понять, почему это объясняется.Созданный UserRole получает передается в *Store, который он реализует, но на самом деле он не делает ничего особенного, кроме как действовать как ссылка. В моей реализации я просто использовал уже установленную ссылку, которая, конечно, создает таблицу сопоставления в базе данных, но не бесцельно раскрывается в приложении. Мне просто любопытно.

Переходим снова, с моими сконфигурированных объектов я пошел вперед и реализовать мои собственные IUserStore и IRoleStore классы творчески называемые EmployeeStore и RoleStore:

public class EmployeeStore : IQueryableUserStore<Employee, int>, IUserStore<Employee, int>, IUserPasswordStore<Employee, int>, IUserRoleStore<Employee, int>, IDisposable { 
    private bool Disposed; 
    private IDatabaseRepository<Role> RolesRepository { get; set; } 
    private IDatabaseRepository<Employee> EmployeesRepository { get; set; } 

    public EmployeeStore(
     IDatabaseRepository<Role> rolesRepository, 
     IDatabaseRepository<Employee> employeesRepository) { 
     this.RolesRepository = rolesRepository; 
     this.EmployeesRepository = employeesRepository; 
    } 

    #region IQueryableUserStore Members 
    public IQueryable<Employee> Users { 
     get { 
      return this.EmployeesRepository.Set; 
     } 
    } 
    #endregion 

    #region IUserStore Members 
    public async Task CreateAsync(
     Employee employee) { 
     this.ThrowIfDisposed(); 

     if (employee == null) { 
      throw new ArgumentNullException("employee"); 
     } 

     await this.EmployeesRepository.AddAndCommitAsync(employee); 
    } 

    public async Task DeleteAsync(
     Employee employee) { 
     this.ThrowIfDisposed(); 

     if (employee == null) { 
      throw new ArgumentNullException("employee"); 
     } 

     await this.EmployeesRepository.RemoveAndCommitAsync(employee); 
    } 

    public Task<Employee> FindByIdAsync(
     int employeeId) { 
     this.ThrowIfDisposed(); 

     return Task.FromResult<Employee>(this.EmployeesRepository.FindSingleOrDefault(
      u => 
       (u.Id == employeeId))); 
    } 

    public Task<Employee> FindByNameAsync(
     string userName) { 
     this.ThrowIfDisposed(); 

     return Task.FromResult<Employee>(this.EmployeesRepository.FindSingleOrDefault(
      e => 
       (e.UserName == userName))); 
    } 

    public async Task UpdateAsync(
     Employee employee) { 
     this.ThrowIfDisposed(); 

     if (employee == null) { 
      throw new ArgumentNullException("employee"); 
     } 

     await this.EmployeesRepository.CommitAsync(); 
    } 
    #endregion 

    #region IDisposable Members 
    public void Dispose() { 
     this.Dispose(true); 

     GC.SuppressFinalize(this); 
    } 

    protected void Dispose(
     bool disposing) { 
     this.Disposed = true; 
    } 

    private void ThrowIfDisposed() { 
     if (this.Disposed) { 
      throw new ObjectDisposedException(base.GetType().Name); 
     } 
    } 
    #endregion 

    #region IUserPasswordStore Members 
    public Task<string> GetPasswordHashAsync(
     Employee employee) { 
     this.ThrowIfDisposed(); 

     if (employee == null) { 
      throw new ArgumentNullException("employee"); 
     } 

     return Task.FromResult<string>(employee.PasswordHash); 
    } 

    public Task<bool> HasPasswordAsync(
     Employee employee) { 
     return Task.FromResult<bool>(!String.IsNullOrEmpty(employee.PasswordHash)); 
    } 

    public Task SetPasswordHashAsync(
     Employee employee, 
     string passwordHash) { 
     this.ThrowIfDisposed(); 

     if (employee == null) { 
      throw new ArgumentNullException("employee"); 
     } 

     employee.PasswordHash = passwordHash; 

     return Task.FromResult<int>(0); 
    } 
    #endregion 

    #region IUserRoleStore Members 
    public Task AddToRoleAsync(
     Employee employee, 
     string roleName) { 
     this.ThrowIfDisposed(); 

     if (employee == null) { 
      throw new ArgumentNullException("employee"); 
     } 

     if (String.IsNullOrEmpty(roleName)) { 
      throw new ArgumentNullException("roleName"); 
     } 

     Role role = this.RolesRepository.FindSingleOrDefault(
      r => 
       (r.Name == roleName)); 

     if (role == null) { 
      throw new InvalidOperationException("Role not found"); 
     } 

     employee.Roles.Add(role); 

     return Task.FromResult<int>(0); 
    } 

    public Task<IList<string>> GetRolesAsync(
     Employee employee) { 
     this.ThrowIfDisposed(); 

     if (employee == null) { 
      throw new ArgumentNullException("employee"); 
     } 

     return Task.FromResult<IList<string>>(employee.Roles.Select(
      r => 
       r.Name).ToList()); 
    } 

    public Task<bool> IsInRoleAsync(
     Employee employee, 
     string roleName) { 
     this.ThrowIfDisposed(); 

     if (employee == null) { 
      throw new ArgumentNullException("employee"); 
     } 

     if (String.IsNullOrEmpty(roleName)) { 
      throw new ArgumentNullException("roleName"); 
     } 

     return Task.FromResult<bool>(employee.Roles.Any(
      r => 
       (r.Name == roleName))); 
    } 

    public Task RemoveFromRoleAsync(
     Employee employee, 
     string roleName) { 
     this.ThrowIfDisposed(); 

     if (employee == null) { 
      throw new ArgumentNullException("employee"); 
     } 

     if (String.IsNullOrEmpty(roleName)) { 
      throw new ArgumentNullException("roleName"); 
     } 

     Role role = this.RolesRepository.FindSingleOrDefault(
      r => 
       (r.Name == roleName)); 

     if (role == null) { 
      throw new InvalidOperationException("Role is null"); 
     } 

     employee.Roles.Remove(role); 

     return Task.FromResult<int>(0); 
    } 
    #endregion 
} 

RoleStore:

public class RoleStore : IQueryableRoleStore<Role, int>, IRoleStore<Role, int>, IDisposable { 
    private bool Disposed; 
    private IDatabaseRepository<Role> RolesRepository { get; set; } 

    public RoleStore(
     IDatabaseRepository<Role> rolesRepository) { 
     this.RolesRepository = rolesRepository; 
    } 

    #region IQueryableRoleStore Members 
    public IQueryable<Role> Roles { 
     get { 
      return this.RolesRepository.Set; 
     } 
    } 
    #endregion 

    #region IRoleStore Members 
    public async Task CreateAsync(
     Role role) { 
     this.ThrowIfDisposed(); 

     if (role == null) { 
      throw new ArgumentNullException("role"); 
     } 

     await this.RolesRepository.AddAndCommitAsync(role); 
    } 

    public async Task DeleteAsync(
     Role role) { 
     this.ThrowIfDisposed(); 

     if (role == null) { 
      throw new ArgumentNullException("role"); 
     } 

     await this.RolesRepository.RemoveAndCommitAsync(role); 
    } 

    public Task<Role> FindByIdAsync(
     int roleId) { 
     this.ThrowIfDisposed(); 

     return Task.FromResult<Role>(this.RolesRepository.FindSingleOrDefault(
      r => 
       (r.Id == roleId))); 
    } 

    public Task<Role> FindByNameAsync(
     string roleName) { 
     this.ThrowIfDisposed(); 

     return Task.FromResult<Role>(this.RolesRepository.FindSingleOrDefault(
      r => 
       (r.Name == roleName))); 
    } 

    public async Task UpdateAsync(
     Role role) { 
     this.ThrowIfDisposed(); 

     if (role == null) { 
      throw new ArgumentNullException("role"); 
     } 

     await this.RolesRepository.CommitAsync(); 
    } 
    #endregion 

    #region IDisposable Members 
    public void Dispose() { 
     this.Dispose(true); 

     GC.SuppressFinalize(this); 
    } 

    protected void Dispose(
     bool disposing) { 
     this.Disposed = true; 
    } 

    private void ThrowIfDisposed() { 
     if (this.Disposed) { 
      throw new ObjectDisposedException(base.GetType().Name); 
     } 
    } 
    #endregion 
} 

Теперь, что я заметил, было то, что реализация Entity Framework создавала то, что выглядело как мини-репозиторий. Поскольку мой проект уже использовал мою собственную репозиториальную реализацию, я решил использовать ее вместо этого. Посмотрим, как это получится ...

Теперь все это работает и на удивление не сбой вообще, или, по крайней мере, еще нет. При этом у меня есть все эти замечательные реализации Identity, но я не могу понять, как использовать их в моем приложении MVC. Так как это выходит за рамки этого вопроса, я пойду вперед и открою новый, который обращается к этому вопросу.

Я оставляю это как ответ на вопрос, если кто-то другой столкнется с этим в будущем. Конечно, если кто-нибудь увидит ошибку в коде, который я опубликовал, сообщите мне.

6

Посмотрите на SimpleSecurity Project source code для примера того, как был расширен контекст базы данных ASP.NET идентичности включить новые таблицы. Это может сработать для вашей ситуации. Вот как новый контекст был определен путем наследования из контекста ASP.NET Identity.

public class SecurityContext : IdentityDbContext<ApplicationUser> 
{ 
    public SecurityContext() 
     : base("SimpleSecurityConnection") 
    { 
    } 


    public DbSet<Resource> Resources { get; set; } 
    public DbSet<OperationsToRoles> OperationsToRoles { get; set; } 

    protected override void OnModelCreating(DbModelBuilder modelBuilder) 
    { 
     base.OnModelCreating(modelBuilder); 
     modelBuilder.Configurations.Add(new ResourceConfiguration()); 
     modelBuilder.Configurations.Add(new OperationsToRolesConfiguration()); 
    } 
} 

SimpleSecurity Project decouples ASP.NET Identity from your MVC application и расширяет его.

Поскольку ваш класс Employee является профилем пользователя для членства, я бы посмотрел на его пошив, чтобы он соответствовал how you customize the user profile in ASP.NET Identity, which is discussed here. В основном ваш класс Employee должен наследовать от IdentityUser, и вы удаляете свойство Password из Employee, так как это определено в IdentityUser, и инфраструктура ищет его там. Тогда при определении вашего контекста вы бы использовать класс Employee вместо так будет выглядеть как этот

public class DatabaseContext : IdentityDbContext<Employee> 
{ 
    ... 
} 
+0

не мог бы вы просто переопределить свойство «пароль» в производном класс? – GFoley83

+0

@ GFoley83 - Какова была бы цель переопределить свойство пароля? –

+0

Ничего особенного, просто семантика действительно. Обычно я предпочитаю, чтобы мои производные классы содержали все свойства, когда базовый класс содержится в другой DLL сборки/третьей стороны, поскольку не всегда понятно/интуитивно понятно, какие свойства вы наследуете. – GFoley83

6

Ни одно решение не подходит для всех ситуаций, но для моего проекта я обнаружил, что проще всего расширить классы IdentityUser и IdentityDbContext. Ниже приведен псевдокод, который фокусируется на минимальном минимуме, который вам нужно будет изменить/добавить, чтобы это работало.

Для вашего класса пользователей:

public class DomainUser : IdentityUser 
{ 
    public DomainUser(string userName) : base(userName) {} 

    public DomainUser() {} 
} 

В целях реализации DbContext:

public class DomainModelContext : IdentityDbContext<DomainUser> 
{ 
    public DomainModelContext() 
     : base() {} 

    public DomainModelContext(string nameOrConnectionString) 
     : base(nameOrConnectionString) {} 

    protected override void OnModelCreating(DbModelBuilder modelBuilder) 
    { 
     base.OnModelCreating(modelBuilder); 
    } 
} 

И в Startup.Auth.cs:

public static Func<UserManager<DomainUser>> UserManagerFactory { get; set; } 

    static Startup() 
    { 
     UserManagerFactory =() => new UserManager<DomainUser>(new UserStore<DomainUser>(new DomainModelContext())); 
    } 

Другим возможным вариантом является создание 1-1 между классом DomainUser и классом ApplicationUser, который наследуется от Identit yUser. Это позволит сократить связь между моделью предметной области и механизмом идентичности, особенно если вы использовали WithRequiredDependent без создания двунаправленного свойства навигации, что-то вроде этого:

modelBuilder.Entity<ApplicationUser>().HasRequired(au => au.DomainUser).WithRequiredPrincipal(); 
Смежные вопросы