将 ASP.NET 标识集成到现有 DbContext 中

Posted

技术标签:

【中文标题】将 ASP.NET 标识集成到现有 DbContext 中【英文标题】:Integrating ASP.NET Identity into Existing DbContext 【发布时间】:2014-02-20 12:53:28 【问题描述】:

我正在 VS2013 中的 .NET 4.5.1 中开发一个 ASP.NET MVC 5 项目,该项目使用 Entity Framework 6 Code-First。我建立了一个相当大的数据库并且有些工作(项目大约两周前)。我现在想集成用户身份验证,但我不知道如何处理它。在花了一天的大部分时间研究之后,我决定给新的 ASP.NET 身份框架一个机会,而不是编写自定义的成员资格或角色提供程序。我很困惑的是如何让它与我现有的数据库/模型一起工作。

目前我有一个名为Employee 的对象,它包含基本的员工信息(目前)。在思考了一整天的问题后,我决定将身份验证与其分离成一个User 对象,这正是 Identity 想要的。话虽这么说,我该如何让它发挥作用?

这是我的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);
    

【问题讨论】:

【参考方案1】:

因此,在花了大约一天左右的时间阅读和阅读之后,我最终构建了自己的 Identity 实现。首先我所做的是获取我现有的Employee 对象并将其扩展为从IUser&lt;int&gt; 继承。 IUser&lt;int&gt; 是一个接口,它是 Identity 2.0(当前处于 alpha 版本)的一部分,它允许将主键类型配置为 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.
    

旁注,由于我不打算使用该属性的 setter,除了简单地将其留空之外,是否有更清洁的方法来废弃它?

继续,我还添加了PasswordHash 属性。我添加了我自己的Role 对象,继承自IRole&lt;int&gt;。最后,EmployeeRole 对象都有一个ICollection&lt;T&gt; 相互链接。另一个方面说明,Identity 的实体框架实现手动创建映射表UserRoles,而不是利用它自己的配置功能,我似乎无法理解其背后的原因。它创建的UserRole 确实会传递到它实现的*Stores 中,但除了充当链接之外,它并没有真正做任何特别的事情。在我的实现中,我只是使用了已经建立的链接,它当然会在数据库中创建一个映射表,但不会毫无意义地暴露在应用程序中。我只是觉得很好奇。

继续前进,使用我配置的对象,我继续实施我自己的 IUserStoreIRoleStore 类,创造性地称为 EmployeeStoreRoleStore

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

现在,我注意到实体框架实现正在创建一个看起来像迷你存储库的东西。由于我的项目已经在使用我自己的 Repository 实现,我决定改用它。我们会看看结果如何......

现在,所有这些都有效,而且令人惊讶的是根本没有崩溃,或者至少现在还没有。话虽如此,我拥有所有这些美妙的身份实现,但我似乎无法弄清楚如何在我的 MVC 应用程序中利用它们。由于这超出了这个问题的范围,我将继续打开一个新的来解决这个问题。

我将此作为问题的答案,以防其他人将来遇到此问题。当然,如果有人在我发布的代码中发现错误,请告诉我。

【讨论】:

【参考方案2】:

查看SimpleSecurity Project source code 示例,了解如何扩展 ASP.NET Identity 的数据库上下文以包含新表。这可能适用于您的情况。这是通过从 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());
    

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 继承,并且您将从 Employee 中删除 Password 属性,因为这是在 IdentityUser 中定义的,并且框架会在那里查找它。然后在定义你的上下文时,你会使用 Employee 类,所以它看起来像这样

public class DatabaseContext : IdentityDbContext<Employee>

  ...

【讨论】:

你不能只覆盖派生类中的“密码”属性吗? @GFoley83 - 覆盖密码属性的目的是什么? 没什么太重要的,只是语义而已。当基类包含在不同的程序集/第 3 方 dll 中时,我通常更喜欢让派生类包含所有属性,因为您实际继承的属性并不总是清晰/直观。【参考方案3】:

没有一种解决方案适合所有情况,但对于我的项目,我发现最简单的方法是扩展 IdentityUserIdentityDbContext 类。以下是伪代码,重点关注您需要更改/添加以使其正常工作的最低限度。

对于您的用户类:

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

另一个可能的选择是在您的 DomainUser 类和继承自 IdentityUser 的 ApplicationUser 类之间创建 1-1 关系。这将减少您的域模型和身份机制之间的耦合,特别是如果您使用 WithRequiredDependent 而不创建双向导航属性,如下所示:

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

【讨论】:

以上是关于将 ASP.NET 标识集成到现有 DbContext 中的主要内容,如果未能解决你的问题,请参考以下文章

将 ASP.NET MVC5 身份验证添加到现有项目

将新的 ASP.NET MVC 应用程序与现有角色集成

如何使用 ASP.NET Core Identity 将登录/注册系统添加到现有数据库?

将 ASP.Net MVC 与 WebForms 相结合

将 jQuery 集成到现有的 ASP.NET Web 应用程序中?

如何将“聊天”集成到 ASP.NET MVC 站点中?