web-dev-qa-db-fra.com

Intégration de l'identité ASP.NET dans DbContext existant

Je travaille sur un projet ASP.NET MVC 5 dans VS2013, .NET 4.5.1, qui utilise Entity Framework 6 Code-First. J'ai une base de données de taille décente construite et quelque peu fonctionnelle (le projet a environ deux semaines). Je veux intégrer l'authentification des utilisateurs maintenant, mais je ne sais pas comment l'aborder. Après avoir passé la majeure partie de la journée à faire des recherches, j'ai décidé de donner au nouveau cadre d'identité ASP.NET une chance de devoir écrire des fournisseurs d'adhésion ou de rôle personnalisés. Ce qui me dérange, c'est comment faire fonctionner tout cela avec la base de données/le modèle existant que j'ai.

Actuellement, j'ai un objet appelé Employee qui contient les informations de base sur les employés (pour l'instant). Après avoir réfléchi toute la journée à la question, j'ai décidé d'en découpler l'authentification en un objet User, ce que veut de toute façon l'identité. Cela étant dit, comment puis-je faire en sorte que tout fonctionne?

Voici ma classe 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>();
    }
}

Et ma classe dérivée 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);
    }
}
24
Gup3rSuR4c

Donc, après avoir passé environ une journée à lire et lire, j'ai fini par construire ma propre implémentation d'identité. Tout d'abord, j'ai pris mon objet Employee existant et je l'ai étendu pour hériter de IUser<int>. IUser<int> est une interface faisant partie d'Identity 2.0 (actuellement en alpha) qui permet de configurer le type de clé primaire sur autre chose que string comme c'était le cas par défaut dans 1.0. En raison de la façon dont je stocke les données, mon implémentation était vraiment spécifique. Par exemple, un Employee peut avoir plusieurs objets Email qui lui sont liés, et pour mon application, je voulais utiliser les e-mails comme noms d'utilisateur. Donc, j'ai simplement défini la propriété UserName pour renvoyer l'e-mail de travail de Employee:

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

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

Note latérale, puisque je n'utiliserai pas le setter pour la propriété, y a-t-il une manière plus propre de l'obsoler autre que de simplement la laisser vide?

En continuant, j'ai également ajouté la propriété PasswordHash. J'ai ajouté mon propre objet Role, héritant de IRole<int>. Enfin, les objets Employee et Role ont chacun un ICollection<T> reliant les uns aux autres. Autre remarque, l'implémentation Entity Framework d'Identity crée manuellement la table de mappage UserRoles plutôt que de tirer parti de ses propres capacités de configuration et je n'arrive pas à comprendre le raisonnement derrière. Le UserRole qu'il crée est passé dans le *Stores il implémente, mais il ne fait rien de spécial autre que d'agir comme un lien. Dans mon implémentation, j'ai simplement utilisé le lien déjà établi, qui crée bien sûr une table de mappage dans la base de données, mais n'est pas inutilement exposé dans l'application. Je trouve ça curieux.

Poursuivant encore, avec mes objets configurés, je suis allé de l'avant et j'ai implémenté mes propres classes IUserStore et IRoleStore de façon créative appelées EmployeeStore et 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
}

Maintenant, ce que j'ai remarqué, c'est que l'implémentation Entity Framework créait ce qui ressemblait à un mini-référentiel. Étant donné que mon projet utilisait déjà ma propre implémentation de référentiel, j'ai décidé de l'utiliser à la place. Nous verrons comment cela se passe ...

Maintenant, tout cela fonctionne et, de façon surprenante, ne plante pas du tout, ou du moins pas encore. Cela étant dit, j'ai toutes ces merveilleuses implémentations d'identité, mais je n'arrive pas à comprendre comment les exploiter dans mon application MVC. Étant donné que cela n'entre pas dans le cadre de cette question, j'irai de l'avant et j'en ouvrirai une nouvelle à ce sujet.

Je laisse cela comme la réponse à la question au cas où quelqu'un d'autre rencontrerait cela à l'avenir. Bien sûr, si quelqu'un voit une erreur dans le code que j'ai publié, faites-le moi savoir.

14
Gup3rSuR4c

Aucune solution ne convient à toutes les situations, mais pour mon projet, j'ai trouvé que la chose la plus simple à faire était d'étendre les classes IdentityUser et IdentityDbContext. Vous trouverez ci-dessous un pseudocode qui se concentre sur le strict minimum que vous devrez modifier/ajouter pour que cela fonctionne.

Pour votre classe d'utilisateurs:

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

    public DomainUser() {}
}

Pour votre implémentation DbContext:

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

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

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

Et dans Startup.Auth.cs:

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

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

Une autre option potentielle consiste à créer une relation 1-1 entre votre classe DomainUser et la classe ApplicationUser qui hérite d'IdentityUser. Cela réduirait le couplage entre votre modèle de domaine et le mécanisme d'identité, surtout si vous avez utilisé WithRequiredDependent sans créer une propriété de navigation bidirectionnelle, quelque chose comme ceci:

modelBuilder.Entity<ApplicationUser>().HasRequired(au => au.DomainUser).WithRequiredPrincipal();
6
joelmdev

Jetez un œil au code source du projet SimpleSecurity pour un exemple de la façon dont le contexte de base de données de ASP.NET Identity a été étendu pour inclure de nouvelles tables. Cela peut fonctionner pour votre situation. Voici comment le nouveau contexte a été défini en héritant du contexte d'identité ASP.NET.

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());
    }
}

Le SimpleSecurity Project dissocie l'identité ASP.NET de votre application MVC et l'étend.

Étant donné que votre classe Employee semble être le profil utilisateur pour l'appartenance, je voudrais l'adapter pour l'adapter à comment vous personnalisez le profil utilisateur dans ASP.NET Identity, qui est discuté ici . Fondamentalement, votre classe Employee doit hériter d'IdentityUser et vous supprimeriez la propriété Password de Employee, car elle est définie dans IdentityUser et le framework la recherche là-bas. Ensuite, lors de la définition de votre contexte, vous utiliseriez la classe Employee à la place afin qu'elle ressemble à ceci

public class DatabaseContext : IdentityDbContext<Employee>
{
  ...
}
6
Kevin Junghans