ASP.NET Identity 2.0:实现基于组的权限管理
今年早些时候,我们探讨了使用 ASP.NET Identity 1.0 框架实现基于组的权限管理。该项目的目标是通过将我们熟悉的 Identity Role 更多地视为一种“权限”,来获得对应用程序授权的更精细的控制。
今年早些时候,我们探讨了使用 ASP.NET Identity 1.0 框架实现基于组的权限管理。该项目的目标是通过将我们熟悉的 Identity Role 更多地视为一种可以授予组成员的“权限”,来获得对应用程序授权的更精细的控制。
随着 2014 年 3 月 Identity 2.0 的发布,出现了一些重大变更,以及功能和复杂性的显著扩展。Identity 2.0 现在是一个用于 ASP.NET 的成熟的授权和认证系统。然而,新增的功能也付出了代价。要开始有效地使用 Identity 2.0,需要理解很多东西。
在之前的文章中,我们已经介绍了一些基础知识
- ASP.NET MVC 和 Identity 2.0:了解基础知识
- ASP.NET Identity 2.0:设置账户验证和双因素授权
- ASP.NET Identity 2.0:自定义用户和角色
- ASP.NET Identity 2.0 扩展身份模型并使用整数键而不是字符串
- ASP.NET Identity 2.0:可扩展模板项目
我们用于在 Identity 1.0 下实现基于组的权限的代码在迁移到 Identity 2.0 时会中断。两个版本之间的变化太多,无法进行干净、轻松的升级。在本文中,我们将重新审视基于组的权限这一想法,并在 Identity 2.0 框架下实现这个概念。
- 开始 - 克隆一个方便的项目模板
- 添加组模型
- 在 DbContext 类中重写 OnModelCreating
- 探究:构建一致的异步模型架构
- 模仿 EntityStore
- 构建 GroupStoreBase 类 - 构建主要的 ApplicationGroupStore 类
- 管理复杂关系 - ApplicationGroupManager 类
- 添加 GroupViewModel
- 更新 EditUserViewModel
- 构建 GroupsAdminController
- 修改 UsersAdminController
- 为 GroupsAdminController 添加视图
- 修改 Identity.Config 文件和 DbInitializer
- 关于授权和安全的一些思考
快速回顾
在熟悉的 ASP.NET Identity 结构下,用户被分配一个或多个角色(Role)。传统上,对某些应用程序功能的访问是通过使用 [Authorize] 特性,将访问特定控制器或控制器方法的权限限制为某些角色的成员来管理的。这在一定程度上是有效的,但不利于对更细粒度的访问权限进行高效管理。
基于组的权限项目试图在一个功能完备、基于声明(Claims-based)的复杂授权方案和 Identity 开箱即用提供的简单(且有限)的授权之间找到一个中间地带。在这里,我们将实现另一个熟悉的概念——用户被分配到一个或多个组(Group)。组被授予一组权限,这些权限对应于组成员执行其职能所需的各种授权。
我在上一篇文章中讨论了总体概念和安全问题,所以这里不再赘述。作为参考,以下主题可能值得快速浏览:
在本文中,我们将使用 Identity 2.0 实现一个类似的结构。
开始 - 克隆一个方便的项目模板
我们将从一个方便、可定制的项目模板开始,该模板基于 Identity 团队创建的 Identity Samples 项目。我利用了我们在过去几篇文章中学到的知识,创建了一个可供扩展的项目,可以从我的 Github 账户克隆(希望很快也能从 Nuget 克隆!)。
或者,克隆该项目的完整源代码
或者,如果您愿意,也可以克隆已完成的“组权限”项目的源代码,它也在 Github 上
如果您从模板项目开始并跟着操作,最好重命名目录和项目文件,以反映当前的工作。如果这样做,请确保也更新 Web.Config 文件。您可能需要更新连接字符串,以便在创建后端数据库时,其名称能反映“组权限”应用程序。
您必须更新 appSettings =>owin:AppStartup
元素,使其启动程序集名称与您在“项目 => 属性 => 程序集名称”中提供的名称匹配。就我而言,我设置了我的连接字符串和owin:appStartup
如下所示:
更新 Web.Config 连接字符串和 owin:appStartup 元素
<connectionStrings>
<add name="DefaultConnection"
connectionString="Data Source=(LocalDb)\v11.0;
Initial Catalog=AspNetIdentity2GroupPermissions-5;
Integrated Security=SSPI"
providerName="System.Data.SqlClient" />
</connectionStrings>
<appSettings>
<add key="owin:AppStartup" value="IdentitySample.Startup,AspNetIdentity2GRoupPermissions" />
<add key="webpages:Version" value="3.0.0.0" />
<add key="webpages:Enabled" value="false" />
<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
</appSettings></configuration>
添加组模型
我们直接开始吧。我们可以先向 Models => IdentityModels.cs 文件中添加一些新模型。我们将在这里定义三个不同的类:ApplicationGroup
,这是核心的组模型,以及另外两个作为ApplicationUser
和ApplicationRole
ApplicationGroup
内集合的映射类。将以下内容添加到 IdentityModels.cs 文件中:
ApplicationGroup 和相关类
public class ApplicationGroup
{
public ApplicationGroup()
{
this.Id = Guid.NewGuid().ToString();
this.ApplicationRoles = new List<ApplicationGroupRole>();
this.ApplicationUsers = new List<ApplicationUserGroup>();
}
public ApplicationGroup(string name)
: this()
{
this.Name = name;
}
public ApplicationGroup(string name, string description)
: this(name)
{
this.Description = description;
}
[Key]
public string Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public virtual ICollection<ApplicationGroupRole> ApplicationRoles { get; set; }
public virtual ICollection<ApplicationUserGroup> ApplicationUsers { get; set; }
}
public class ApplicationUserGroup
{
public string ApplicationUserId { get; set; }
public string ApplicationGroupId { get; set; }
}
public class ApplicationGroupRole
{
public string ApplicationGroupId { get; set; }
public string ApplicationRoleId { get; set; }
}
组与用户和角色都存在多对多的关系。一个用户可以属于零个或多个组,一个组可以有零个或多个用户。角色也是如此。一个角色可以分配给零个或多个组,一个组可以拥有零个或多个角色。
当我们需要使用 EntityFramework 管理这种类型的关系时,我们可以创建本质上是映射类的东西,在本例中是ApplicationUserGroup
, and和 ApplicationGroupRole
。我们这样做的方式类似于 Identity 团队在定义用户、角色和用户角色时使用的结构。例如,我们的ApplicationUser
类派生自IdentityUser
,它定义了一个 Roles 属性。请注意,IdentityUser
的 Roles 属性不是一个IdentityRole
对象的集合,而是一个IdentityUserRole
对象的集合。不同之处在于,IdentityUserRole
类只定义了一个UserId
属性和一个RoleId
属性。
我们在这里做同样的事情。我们需要通过在ApplicationGroup
上定义的集合与每个关系中涉及的领域对象之间添加映射类,来让 EF 管理我们上面描述的多对多关系。
在 DbContext 类中重写 OnModelCreating
EntityFramework 不会自动弄清楚我们的多对多关系,也不会确定在数据库中创建正确的表结构。我们需要通过在ApplicationDbContext
类中重写 OnModelCreating 方法来帮助它。此外,我们需要将ApplicationGroups
作为我们 DbContext 的一个属性添加,这样我们就可以在应用程序内部访问我们的组了。 按如下方式更新ApplicationDbContext
类:
更新 ApplicationDbContext 并重写 OnModelCreating
public class ApplicationDbContext
: IdentityDbContext<ApplicationUser, ApplicationRole,
string, ApplicationUserLogin, ApplicationUserRole, ApplicationUserClaim>
{
public ApplicationDbContext()
: base("DefaultConnection")
{
}
static ApplicationDbContext()
{
Database.SetInitializer<ApplicationDbContext>(new ApplicationDbInitializer());
}
public static ApplicationDbContext Create()
{
return new ApplicationDbContext();
}
// Add the ApplicationGroups property:
public virtual IDbSet<ApplicationGroup> ApplicationGroups { get; set; }
// Override OnModelsCreating:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// Make sure to call the base method first:
base.OnModelCreating(modelBuilder);
// Map Users to Groups:
modelBuilder.Entity<ApplicationGroup>()
.HasMany<ApplicationUserGroup>((ApplicationGroup g) => g.ApplicationUsers)
.WithRequired()
.HasForeignKey<string>((ApplicationUserGroup ag) => ag.ApplicationGroupId);
modelBuilder.Entity<ApplicationUserGroup>()
.HasKey((ApplicationUserGroup r) =>
new
{
ApplicationUserId = r.ApplicationUserId,
ApplicationGroupId = r.ApplicationGroupId
}).ToTable("ApplicationUserGroups");
// Map Roles to Groups:
modelBuilder.Entity<ApplicationGroup>()
.HasMany<ApplicationGroupRole>((ApplicationGroup g) => g.ApplicationRoles)
.WithRequired()
.HasForeignKey<string>((ApplicationGroupRole ap) => ap.ApplicationGroupId);
modelBuilder.Entity<ApplicationGroupRole>().HasKey((ApplicationGroupRole gr) =>
new
{
ApplicationRoleId = gr.ApplicationRoleId,
ApplicationGroupId = gr.ApplicationGroupId
}).ToTable("ApplicationGroupRoles");
}
}
有了这些简单的开端,让我们运行项目,看看一切是否按预期工作。
运行项目并确认数据库创建
如果我们现在运行项目,将会看到标准的 MVC 页面。回想一下,使用 Entity Framework Code-First 模型,数据库的创建将在首次访问数据时发生。换句话说,就目前情况而言,我们需要登录。
到目前为止,我们还没有为前端网站添加任何明确的功能——当我们登录时,不会有任何证据表明我们的底层模型已经改变。我们只是想看看网站是否能正常启动,以及我们期望的数据库和表是否确实被创建了。
一旦我们运行了项目并登录,我们应该能够停下来,并使用 Visual Studio 服务器资源管理器来查看我们的数据库表是什么样子。我们应该看到类似下面的内容:
服务器资源管理器中新增的组表
从上图我们可以看到,我们的ApplicationGroup
及相关类现在由我们后端数据库中的表来表示,并且包含了预期的列和主键。到目前为止,一切顺利!
探究:构建一致的异步模型架构
ASP.NET Identity 2.0 提供了一个完全异步的模型架构。我们将尽最大努力遵循 Identity 团队在构建我们的组管理结构时建立的约定,使用类似的抽象来创建一个具有完全异步方法的组存储(Group store)和组管理器(Group Manager)。换句话说,也许我们可以看看 Identity 2.0 团队是如何构建基本的 UserStore 和 RoleStore 抽象(包括异步方法)的,然后简单地仿照它们来构建我们自己的 GroupStore。
如果我们仔细研究 Identity 团队用来构建基本的UserStore
和UserStore 和 RoleStore
类的结构,我们会发现它们都是围绕一个名为EntityStore<TEntity>
的类的实例构建的,该类封装了持久化存储所期望的最基本行为。
例如,如果我们查看RoleStore<TRole, TKey, TUserRole>
类(定义在 Identity 2.0 框架中),我们会发现以下内容:
分解 RoleStore 类
public class RoleStore<TRole, TKey, TUserRole>
: IQueryableRoleStore<TRole, TKey>, IRoleStore<TRole, TKey>, IDisposable
where TRole : IdentityRole<TKey, TUserRole>, new()
where TUserRole : IdentityUserRole<TKey>, new()
{
private bool _disposed;
private EntityStore<TRole> _roleStore;
public DbContext Context { get; private set; }
public bool DisposeContext {get; set; }
public IQueryable<TRole> Roles
{
get
{
return this._roleStore.EntitySet;
}
}
public RoleStore(DbContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
this.Context = context;
this._roleStore = new EntityStore<TRole>(context);
}
public virtual async Task CreateAsync(TRole role)
{
this.ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException("role");
}
this._roleStore.Create(role);
TaskExtensions.CultureAwaiter<int> cultureAwaiter =
this.Context.SaveChangesAsync().WithCurrentCulture<int>();
await cultureAwaiter;
}
public virtual async Task DeleteAsync(TRole role)
{
this.ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException("role");
}
this._roleStore.Delete(role);
TaskExtensions.CultureAwaiter<int> cultureAwaiter =
this.Context.SaveChangesAsync().WithCurrentCulture<int>();
await cultureAwaiter;
}
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (this.DisposeContext && disposing && this.Context != null)
{
this.Context.Dispose();
}
this._disposed = true;
this.Context = null;
this._roleStore = null;
}
public Task<TRole> FindByIdAsync(TKey roleId)
{
this.ThrowIfDisposed();
return this._roleStore.GetByIdAsync(roleId);
}
public Task<TRole> FindByNameAsync(string roleName)
{
this.ThrowIfDisposed();
return QueryableExtensions
.FirstOrDefaultAsync<TRole>(this._roleStore.EntitySet,
(TRole u) => u.Name.ToUpper() == roleName.ToUpper());
}
private void ThrowIfDisposed()
{
if (this._disposed)
{
throw new ObjectDisposedException(this.GetType().Name);
}
}
public virtual async Task UpdateAsync(TRole role)
{
this.ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException("role");
}
this._roleStore.Update(role);
TaskExtensions.CultureAwaiter<int> cultureAwaiter =
this.Context.SaveChangesAsync().WithCurrentCulture<int>();
await cultureAwaiter;
}
}
上面的代码很有趣,我们稍后会更仔细地研究。目前,请注意高亮显示的项目。UserStore 和 RoleStore
RoleStore 封装了一个EntityStore<TRole>
的实例。如果我们再深入一点,我们会找到EntityStore
的定义:
来自 Identity 2.0 框架的 EntityStore 类
internal class EntityStore<TEntity>
where TEntity : class
{
public DbContext Context { get; private set; }
public DbSet<TEntity> DbEntitySet { get; private set; }
public IQueryable<TEntity> EntitySet
{
get
{
return this.DbEntitySet;
}
}
public EntityStore(DbContext context)
{
this.Context = context;
this.DbEntitySet = context.Set<TEntity>();
}
public void Create(TEntity entity)
{
this.DbEntitySet.Add(entity);
}
public void Delete(TEntity entity)
{
this.DbEntitySet.Remove(entity);
}
public virtual Task<TEntity> GetByIdAsync(object id)
{
return this.DbEntitySet.FindAsync(new object[] { id });
}
public virtual void Update(TEntity entity)
{
if (entity != null)
{
this.Context.Entry<TEntity>(entity).State = EntityState.Modified;
}
}
尽管代码很简单,但它也非常有趣。不幸的是,我们不能在我们的项目中直接使用EntityStore
EntityStore 类。注意类声明中的 internal 修饰符——这意味着EntityStore
EntityStore 只能被Microsoft.AspNet.Identity.EntityFramework
Microsoft.AspNet.Identity.EntityFramework 程序集中的类访问。换句话说,我们不能使用EntityStore
EntityStore 来构建我们自己的GroupStore
实现。因此,我们将采取历史悠久的“窃取/复制”方法。
构建一个异步的 GroupStore
我们将应用 Identity 团队在构建GroupStore
GroupStore 类时使用的相同约定,然后以类似的方式,将一个GroupManager
类封装在它外面,就像 Identity 框架将一个RoleManager
类封装在一个UserStore 和 RoleStore
.
RoleStore 实例外一样。但首先,我们需要处理EntityStore
EntityStore 问题。为了正确模仿构建 RoleStore 和UserStore
UserStore 所使用的结构,我们基本上需要创建我们自己的EntityStore
EntityStore 实现。在我们的案例中,我们不需要一个泛型类型化的类——我们可以创建一个非泛型的、特定于我们需求的实现。
模仿 EntityStore- 构建 GroupStoreBase 类
我们基本上可以“窃取”上面显示的EntityStore<TEntity>
EntityStore 类的大部分代码,并通过移除类本身的泛型类型参数以及在需要时传递非泛型参数来适应我们的需求。在项目的 Models 文件夹中添加一个名为GroupStoreBase
的类,然后使用以下代码作为类本身。首先,您需要在代码文件顶部添加以下 using 语句:
GroupStoreBase 类所需的程序集引用
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;
GroupStoreBase 类
public class GroupStoreBase
{
public DbContext Context { get; private set; }
public DbSet<ApplicationGroup> DbEntitySet { get; private set; }
public IQueryable<ApplicationGroup> EntitySet
{
get
{
return this.DbEntitySet;
}
}
public GroupStoreBase(DbContext context)
{
this.Context = context;
this.DbEntitySet = context.Set<ApplicationGroup>();
}
public void Create(ApplicationGroup entity)
{
this.DbEntitySet.Add(entity);
}
public void Delete(ApplicationGroup entity)
{
this.DbEntitySet.Remove(entity);
}
public virtual Task<ApplicationGroup> GetByIdAsync(object id)
{
return this.DbEntitySet.FindAsync(new object[] { id });
}
public virtual ApplicationGroup GetById(object id)
{
return this.DbEntitySet.Find(new object[] { id });
}
public virtual void Update(ApplicationGroup entity)
{
if (entity != null)
{
this.Context.Entry<ApplicationGroup>(entity).State = EntityState.Modified;
}
}
}
注意这里的结构。GroupStoreBase
GroupStoreBase 提供了处理DbSet<ApplicationGroup>
的方法,但它不直接对后端数据库进行持久化。持久化将由我们结构中的下一个类——ApplicationGroupStore
.
构建主要的 ApplicationGroupStore 类
——来控制。遵循UserStore
UserStore 和 RoleStore 使用的模式,我们现在将构建一个GroupStore
ApplicationGroupStore 类,它将围绕我们新的GroupStoreBase
GroupStoreBase 类构建。在 models 文件夹中添加另一个名为ApplicationGroupStore
ApplicationGroupStore 的类,并添加以下代码:
ApplicationGroupStore 类
public class ApplicationGroupStore : IDisposable
{
private bool _disposed;
private GroupStoreBase _groupStore;
public ApplicationGroupStore(DbContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
this.Context = context;
this._groupStore = new GroupStoreBase(context);
}
public IQueryable<ApplicationGroup> Groups
{
get
{
return this._groupStore.EntitySet;
}
}
public DbContext Context
{
get;
private set;
}
public virtual void Create(ApplicationGroup group)
{
this.ThrowIfDisposed();
if (group == null)
{
throw new ArgumentNullException("group");
}
this._groupStore.Create(group);
this.Context.SaveChanges();
}
public virtual async Task CreateAsync(ApplicationGroup group)
{
this.ThrowIfDisposed();
if (group == null)
{
throw new ArgumentNullException("group");
}
this._groupStore.Create(group);
await this.Context.SaveChangesAsync();
}
public virtual async Task DeleteAsync(ApplicationGroup group)
{
this.ThrowIfDisposed();
if (group == null)
{
throw new ArgumentNullException("group");
}
this._groupStore.Delete(group);
await this.Context.SaveChangesAsync();
}
public virtual void Delete(ApplicationGroup group)
{
this.ThrowIfDisposed();
if (group == null)
{
throw new ArgumentNullException("group");
}
this._groupStore.Delete(group);
this.Context.SaveChanges();
}
public Task<ApplicationGroup> FindByIdAsync(string roleId)
{
this.ThrowIfDisposed();
return this._groupStore.GetByIdAsync(roleId);
}
public ApplicationGroup FindById(string roleId)
{
this.ThrowIfDisposed();
return this._groupStore.GetById(roleId);
}
public Task<ApplicationGroup> FindByNameAsync(string groupName)
{
this.ThrowIfDisposed();
return QueryableExtensions
.FirstOrDefaultAsync<ApplicationGroup>(this._groupStore.EntitySet,
(ApplicationGroup u) => u.Name.ToUpper() == groupName.ToUpper());
}
public virtual async Task UpdateAsync(ApplicationGroup group)
{
this.ThrowIfDisposed();
if (group == null)
{
throw new ArgumentNullException("group");
}
this._groupStore.Update(group);
await this.Context.SaveChangesAsync();
}
public virtual void Update(ApplicationGroup group)
{
this.ThrowIfDisposed();
if (group == null)
{
throw new ArgumentNullException("group");
}
this._groupStore.Update(group);
this.Context.SaveChanges();
}
// DISPOSE STUFF: ===============================================
public bool DisposeContext
{
get;
set;
}
private void ThrowIfDisposed()
{
if (this._disposed)
{
throw new ObjectDisposedException(this.GetType().Name);
}
}
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (this.DisposeContext && disposing && this.Context != null)
{
this.Context.Dispose();
}
this._disposed = true;
this.Context = null;
this._groupStore = null;
}
}
关于ApplicationGroupStore
ApplicationGroupStore 类,有几点需要注意。首先,请注意这个类通过调用Context.SaveChanges()
或和 Context.SaveChangesAsync()
来处理所有到后端存储的实际持久化操作。此外,我们为每个方法都提供了异步和同步两种实现。
然而,我们还没有完成。虽然ApplicationGroupStore
ApplicationGroupStore 管理着组的基本持久化,但它没有处理由组、用户和角色之间的关系引入的复杂性。在这里,我们可以对我们的组执行基本的 CRUD 操作,但我们无法控制这些类之间的关系。
这就成了ApplicationGroupManager
类。
管理复杂关系 - ApplicationGroupManager 类
的工作了。就我们的应用程序而言,用户、组和角色之间的关系比它们初看起来要复杂得多。
我们的用户-组-角色结构实际上在制造一种错觉。当我们完成后,看起来角色会“属于”组,而用户通过成为某个特定组的成员,获得了该组角色的访问权限。然而,实际情况是,当一个用户被分配到一个特定的组时,我们的应用程序会接着将该用户添加到该组内的每个角色中。
这是一个微妙但重要的区别。
假设我们有一个现有的组,分配了两个角色——“CanEditAccount”和“CanViewAccount”。再假设这个组里有三个用户。最后,假设我们想给这个组添加另一个(已存在的)角色——“CanDeleteAccount”。需要发生什么?
- 我们将角色分配给组
- 我们需要将组的每个成员添加到新角色中
表面上看,这相对直接。然而,每个用户可以属于多个组。而且,一个角色可以被分配给多个组。如果我们想从一个组中移除一个角色呢?
- 从角色中移除组里的每个用户,除非他们同时是另一个也拥有该角色的组的成员
- 从组中移除该角色
这稍微复杂一些。如果我们想从一个组中移除一个用户,也会出现类似的情况:
- 从该组的所有角色中移除该用户,除非该用户也属于另一个拥有相同角色的组
- 从该组中移除该用户
等等。为了让我们的应用程序能够提供最终用户所期望的可预测、直观的行为,背后发生的事情比表面上看到的要多。
ApplicationGroupManager 的工作就是为我们处理这些类型的问题,并提供一个清晰的 API,让我们的控制器可以在用户和后端数据之间工作。ApplicationGroupManager
我们已经创建了 GroupStore 类来处理组数据的基本持久化,并且我们有
UserManager 和 RoleManager 类来处理用户、角色和持久化之间的关系。在大多数情况下,ApplicationUserManager
和ApplicationRoleManager
ApplicationGroupManager 的主要工作将是管理这三个存储之间的交互,偶尔也会直接与 DbContext 交互。我们将通过定义一个 API 来实现这一点,该 API 类似于 Identity 基类
UserManager 和 RoleManager,为我们提供了处理基于组的角色管理所需的直观方法。我们将提供同步和异步两种实现。
在 Models 文件夹中添加另一个类,并将其命名为UserManager
和RoleManager
ApplicationGroupManager。您将需要在代码文件顶部添加以下
usingApplicationGroupManager
语句:using
ApplicationGroupManager 所需的程序集引用
现在,添加以下代码:
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
ApplicationGroupManager 类
正如我们从上面看到的,代码量很大。然而,其中大部分是由于同步和异步方法实现之间的重复。有了以上代码,我们现在有了一个可以从控制器调用的 API,可以开始处理正事,为我们的网站添加组功能了。
public class ApplicationGroupManager
{
private ApplicationGroupStore _groupStore;
private ApplicationDbContext _db;
private ApplicationUserManager _userManager;
private ApplicationRoleManager _roleManager;
public ApplicationGroupManager()
{
_db = HttpContext.Current
.GetOwinContext().Get<ApplicationDbContext>();
_userManager = HttpContext.Current
.GetOwinContext().GetUserManager<ApplicationUserManager>();
_roleManager = HttpContext.Current
.GetOwinContext().Get<ApplicationRoleManager>();
_groupStore = new ApplicationGroupStore(_db);
}
public IQueryable<ApplicationGroup> Groups
{
get
{
return _groupStore.Groups;
}
}
public async Task<IdentityResult> CreateGroupAsync(ApplicationGroup group)
{
await _groupStore.CreateAsync(group);
return IdentityResult.Success;
}
public IdentityResult CreateGroup(ApplicationGroup group)
{
_groupStore.Create(group);
return IdentityResult.Success;
}
public IdentityResult SetGroupRoles(string groupId, params string[] roleNames)
{
// Clear all the roles associated with this group:
var thisGroup = this.FindById(groupId);
thisGroup.ApplicationRoles.Clear();
_db.SaveChanges();
// Add the new roles passed in:
var newRoles = _roleManager.Roles.Where(r => roleNames.Any(n => n == r.Name));
foreach(var role in newRoles)
{
thisGroup.ApplicationRoles.Add(new ApplicationGroupRole
{
ApplicationGroupId = groupId,
ApplicationRoleId = role.Id
});
}
_db.SaveChanges();
// Reset the roles for all affected users:
foreach(var groupUser in thisGroup.ApplicationUsers)
{
this.RefreshUserGroupRoles(groupUser.ApplicationUserId);
}
return IdentityResult.Success;
}
public async Task<IdentityResult> SetGroupRolesAsync(
string groupId, params string[] roleNames)
{
// Clear all the roles associated with this group:
var thisGroup = await this.FindByIdAsync(groupId);
thisGroup.ApplicationRoles.Clear();
await _db.SaveChangesAsync();
// Add the new roles passed in:
var newRoles = _roleManager.Roles
.Where(r => roleNames.Any(n => n == r.Name));
foreach (var role in newRoles)
{
thisGroup.ApplicationRoles.Add(new ApplicationGroupRole
{
ApplicationGroupId = groupId,
ApplicationRoleId = role.Id
});
}
await _db.SaveChangesAsync();
// Reset the roles for all affected users:
foreach (var groupUser in thisGroup.ApplicationUsers)
{
await this.RefreshUserGroupRolesAsync(groupUser.ApplicationUserId);
}
return IdentityResult.Success;
}
public async Task<IdentityResult> SetUserGroupsAsync(
string userId, params string[] groupIds)
{
// Clear current group membership:
var currentGroups = await this.GetUserGroupsAsync(userId);
foreach (var group in currentGroups)
{
group.ApplicationUsers
.Remove(group.ApplicationUsers
.FirstOrDefault(gr => gr.ApplicationUserId == userId
));
}
await _db.SaveChangesAsync();
// Add the user to the new groups:
foreach (string groupId in groupIds)
{
var newGroup = await this.FindByIdAsync(groupId);
newGroup.ApplicationUsers.Add(new ApplicationUserGroup
{
ApplicationUserId = userId,
ApplicationGroupId = groupId
});
}
await _db.SaveChangesAsync();
await this.RefreshUserGroupRolesAsync(userId);
return IdentityResult.Success;
}
public IdentityResult SetUserGroups(string userId, params string[] groupIds)
{
// Clear current group membership:
var currentGroups = this.GetUserGroups(userId);
foreach(var group in currentGroups)
{
group.ApplicationUsers
.Remove(group.ApplicationUsers
.FirstOrDefault(gr => gr.ApplicationUserId == userId
));
}
_db.SaveChanges();
// Add the user to the new groups:
foreach(string groupId in groupIds)
{
var newGroup = this.FindById(groupId);
newGroup.ApplicationUsers.Add(new ApplicationUserGroup
{
ApplicationUserId = userId,
ApplicationGroupId = groupId
});
}
_db.SaveChanges();
this.RefreshUserGroupRoles(userId);
return IdentityResult.Success;
}
public IdentityResult RefreshUserGroupRoles(string userId)
{
var user = _userManager.FindById(userId);
if(user == null)
{
throw new ArgumentNullException("User");
}
// Remove user from previous roles:
var oldUserRoles = _userManager.GetRoles(userId);
if(oldUserRoles.Count > 0)
{
_userManager.RemoveFromRoles(userId, oldUserRoles.ToArray());
}
// Find teh roles this user is entitled to from group membership:
var newGroupRoles = this.GetUserGroupRoles(userId);
// Get the damn role names:
var allRoles = _roleManager.Roles.ToList();
var addTheseRoles = allRoles
.Where(r => newGroupRoles.Any(gr => gr.ApplicationRoleId == r.Id
));
var roleNames = addTheseRoles.Select(n => n.Name).ToArray();
// Add the user to the proper roles
_userManager.AddToRoles(userId, roleNames);
return IdentityResult.Success;
}
public async Task<IdentityResult> RefreshUserGroupRolesAsync(string userId)
{
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
throw new ArgumentNullException("User");
}
// Remove user from previous roles:
var oldUserRoles = await _userManager.GetRolesAsync(userId);
if (oldUserRoles.Count > 0)
{
await _userManager.RemoveFromRolesAsync(userId, oldUserRoles.ToArray());
}
// Find the roles this user is entitled to from group membership:
var newGroupRoles = await this.GetUserGroupRolesAsync(userId);
// Get the damn role names:
var allRoles = await _roleManager.Roles.ToListAsync();
var addTheseRoles = allRoles
.Where(r => newGroupRoles.Any(gr => gr.ApplicationRoleId == r.Id
));
var roleNames = addTheseRoles.Select(n => n.Name).ToArray();
// Add the user to the proper roles
await _userManager.AddToRolesAsync(userId, roleNames);
return IdentityResult.Success;
}
public async Task<IdentityResult> DeleteGroupAsync(string groupId)
{
var group = await this.FindByIdAsync(groupId);
if (group == null)
{
throw new ArgumentNullException("User");
}
var currentGroupMembers = (await this.GetGroupUsersAsync(groupId)).ToList();
// remove the roles from the group:
group.ApplicationRoles.Clear();
// Remove all the users:
group.ApplicationUsers.Clear();
// Remove the group itself:
_db.ApplicationGroups.Remove(group);
await _db.SaveChangesAsync();
// Reset all the user roles:
foreach (var user in currentGroupMembers)
{
await this.RefreshUserGroupRolesAsync(user.Id);
}
return IdentityResult.Success;
}
public IdentityResult DeleteGroup(string groupId)
{
var group = this.FindById(groupId);
if(group == null)
{
throw new ArgumentNullException("User");
}
var currentGroupMembers = this.GetGroupUsers(groupId).ToList();
// remove the roles from the group:
group.ApplicationRoles.Clear();
// Remove all the users:
group.ApplicationUsers.Clear();
// Remove the group itself:
_db.ApplicationGroups.Remove(group);
_db.SaveChanges();
// Reset all the user roles:
foreach(var user in currentGroupMembers)
{
this.RefreshUserGroupRoles(user.Id);
}
return IdentityResult.Success;
}
public async Task<IdentityResult> UpdateGroupAsync(ApplicationGroup group)
{
await _groupStore.UpdateAsync(group);
foreach (var groupUser in group.ApplicationUsers)
{
await this.RefreshUserGroupRolesAsync(groupUser.ApplicationUserId);
}
return IdentityResult.Success;
}
public IdentityResult UpdateGroup(ApplicationGroup group)
{
_groupStore.Update(group);
foreach(var groupUser in group.ApplicationUsers)
{
this.RefreshUserGroupRoles(groupUser.ApplicationUserId);
}
return IdentityResult.Success;
}
public IdentityResult ClearUserGroups(string userId)
{
return this.SetUserGroups(userId, new string[] { });
}
public async Task<IdentityResult> ClearUserGroupsAsync(string userId)
{
return await this.SetUserGroupsAsync(userId, new string[] { });
}
public async Task<IEnumerable<ApplicationGroup>> GetUserGroupsAsync(string userId)
{
var result = new List<ApplicationGroup>();
var userGroups = (from g in this.Groups
where g.ApplicationUsers
.Any(u => u.ApplicationUserId == userId)
select g).ToListAsync();
return await userGroups;
}
public IEnumerable<ApplicationGroup> GetUserGroups(string userId)
{
var result = new List<ApplicationGroup>();
var userGroups = (from g in this.Groups
where g.ApplicationUsers
.Any(u => u.ApplicationUserId == userId)
select g).ToList();
return userGroups;
}
public async Task<IEnumerable<ApplicationRole>> GetGroupRolesAsync(
string groupId)
{
var grp = await _db.ApplicationGroups
.FirstOrDefaultAsync(g => g.Id == groupId);
var roles = await _roleManager.Roles.ToListAsync();
var groupRoles = (from r in roles
where grp.ApplicationRoles
.Any(ap => ap.ApplicationRoleId == r.Id)
select r).ToList();
return groupRoles;
}
public IEnumerable<ApplicationRole> GetGroupRoles(string groupId)
{
var grp = _db.ApplicationGroups.FirstOrDefault(g => g.Id == groupId);
var roles = _roleManager.Roles.ToList();
var groupRoles = from r in roles
where grp.ApplicationRoles
.Any(ap => ap.ApplicationRoleId == r.Id)
select r;
return groupRoles;
}
public IEnumerable<ApplicationUser> GetGroupUsers(string groupId)
{
var group = this.FindById(groupId);
var users = new List<ApplicationUser>();
foreach (var groupUser in group.ApplicationUsers)
{
var user = _db.Users.Find(groupUser.ApplicationUserId);
users.Add(user);
}
return users;
}
public async Task<IEnumerable<ApplicationUser>> GetGroupUsersAsync(string groupId)
{
var group = await this.FindByIdAsync(groupId);
var users = new List<ApplicationUser>();
foreach (var groupUser in group.ApplicationUsers)
{
var user = await _db.Users
.FirstOrDefaultAsync(u => u.Id == groupUser.ApplicationUserId);
users.Add(user);
}
return users;
}
public IEnumerable<ApplicationGroupRole> GetUserGroupRoles(string userId)
{
var userGroups = this.GetUserGroups(userId);
var userGroupRoles = new List<ApplicationGroupRole>();
foreach(var group in userGroups)
{
userGroupRoles.AddRange(group.ApplicationRoles.ToArray());
}
return userGroupRoles;
}
public async Task<IEnumerable<ApplicationGroupRole>> GetUserGroupRolesAsync(
string userId)
{
var userGroups = await this.GetUserGroupsAsync(userId);
var userGroupRoles = new List<ApplicationGroupRole>();
foreach (var group in userGroups)
{
userGroupRoles.AddRange(group.ApplicationRoles.ToArray());
}
return userGroupRoles;
}
public async Task<ApplicationGroup> FindByIdAsync(string id)
{
return await _groupStore.FindByIdAsync(id);
}
public ApplicationGroup FindById(string id)
{
return _groupStore.FindById(id);
}
}
在从组中添加和删除用户、向组中添加和删除角色等方面,我们都力求简单。如您所见,每次我们更改用户所属的组时,我们都通过调用
SetUserGroups()并传入一个组 ID 数组来一次性更改所有组分配。类似地,我们通过调用
SetGroupRoles()来一次性将所有角色分配给一个组,同样传入一个角色名称数组,代表分配给特定组的所有角色。
我们这样做是因为,当我们修改用户的组成员身份时,我们基本上需要重新设置用户的所有角色。同样,当我们修改分配给特定组的角色时,我们需要刷新该组内每个用户分配到的角色。
这样做也很方便,因为当我们从任一管理视图接收用户和/或角色的选择时,我们得到的也是一个数组。我们稍后会更详细地看到这一点。
我们将需要一个视图模型(view model)用于在各个控制器方法和其关联的视图之间传递组数据。在 Models => AdminViewModel.cs 文件中,添加以下类:
添加 GroupViewModel
GroupViewModel 类
请注意,这里我们传递
public class GroupViewModel
{
public GroupViewModel()
{
this.UsersList = new List<SelectListItem>();
this.PermissionsList = new List<SelectListItem>();
}
[Required(AllowEmptyStrings = false)]
public string Id { get; set; }
[Required(AllowEmptyStrings = false)]
public string Name { get; set; }
public string Description { get; set; }
public ICollection<SelectListItem> UsersList { get; set; }
public ICollection<SelectListItem> RolesList { get; set; }
}
ICollection<SelectListItem>来表示分配给一个组的用户和角色。这样,我们可以将用户列表或角色列表传递给视图,允许用户从列表中选择一项或多项,然后在表单数据通过 HTTP POST 方法提交回控制器时处理所做的选择。
既然我们已经打开了 AdminViewModel.cs 文件,我们顺便也修改一下
更新 EditUserViewModel
EditUserViewModel,为它添加一个用于组的集合属性:
向 EditUserViewModel 添加 GroupsList 属性
我们也将保留
public class EditUserViewModel
{
public EditUserViewModel()
{
this.RolesList = new List<SelectListItem>();
this.GroupsList = new List<SelectListItem>();
}
public string Id { get; set; }
[Required(AllowEmptyStrings = false)]
[Display(Name = "Email")]
[EmailAddress]
public string Email { get; set; }
// We will still use this, so leave it here:
public ICollection<SelectListItem> RolesList { get; set; }
// Add a GroupsList Property:
public ICollection<SelectListItem> GroupsList { get; set; }
}
RolesList集合。尽管我们不再直接给用户分配角色,但我们可能想创建一个视图,让我们能看到一个用户因其在不同组中的成员身份而拥有的角色。这样,我们就可以使用同一个 ViewModel。
既然我们的 ViewModel 已经调整好了,让我们添加一个
GroupsAdminController。类似于现有的
.
构建 GroupsAdminController
UserAdminController和 RolesAdminController
和,我们需要提供一个控制器来处理我们新的组功能。
我们需要在 Controllers 目录下添加一个控制器。不要使用 上下文菜单 => 添加控制器 的方法,直接添加一个名为
GroupsAdminController.cs 的类,并添加以下代码:。类似于现有的
ApplicationGroupStore 的类,并添加以下代码:
添加 GroupsAdminController
public class GroupsAdminController : Controller
{
private ApplicationDbContext db = new ApplicationDbContext();
private ApplicationGroupManager _groupManager;
public ApplicationGroupManager GroupManager
{
get
{
return _groupManager ?? new ApplicationGroupManager();
}
private set
{
_groupManager = value;
}
}
private ApplicationRoleManager _roleManager;
public ApplicationRoleManager RoleManager
{
get
{
return _roleManager ?? HttpContext.GetOwinContext()
.Get<ApplicationRoleManager>();
}
private set
{
_roleManager = value;
}
}
public ActionResult Index()
{
return View(this.GroupManager.Groups.ToList());
}
public async Task<ActionResult> Details(string id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
ApplicationGroup applicationgroup =
await this.GroupManager.Groups.FirstOrDefaultAsync(g => g.Id == id);
if (applicationgroup == null)
{
return HttpNotFound();
}
var groupRoles = this.GroupManager.GetGroupRoles(applicationgroup.Id);
string[] RoleNames = groupRoles.Select(p => p.Name).ToArray();
ViewBag.RolesList = RoleNames;
ViewBag.RolesCount = RoleNames.Count();
return View(applicationgroup);
}
public ActionResult Create()
{
//Get a SelectList of Roles to choose from in the View:
ViewBag.RolesList = new SelectList(
this.RoleManager.Roles.ToList(), "Id", "Name");
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create(
[Bind(Include = "Name,Description")] ApplicationGroup applicationgroup,
params string[] selectedRoles)
{
if (ModelState.IsValid)
{
// Create the new Group:
var result = await this.GroupManager.CreateGroupAsync(applicationgroup);
if (result.Succeeded)
{
selectedRoles = selectedRoles ?? new string[] { };
// Add the roles selected:
await this.GroupManager
.SetGroupRolesAsync(applicationgroup.Id, selectedRoles);
}
return RedirectToAction("Index");
}
// Otherwise, start over:
ViewBag.RoleId = new SelectList(
this.RoleManager.Roles.ToList(), "Id", "Name");
return View(applicationgroup);
}
public async Task<ActionResult> Edit(string id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
ApplicationGroup applicationgroup = await this.GroupManager.FindByIdAsync(id);
if (applicationgroup == null)
{
return HttpNotFound();
}
// Get a list, not a DbSet or queryable:
var allRoles = await this.RoleManager.Roles.ToListAsync();
var groupRoles = await this.GroupManager.GetGroupRolesAsync(id);
var model = new GroupViewModel()
{
Id = applicationgroup.Id,
Name = applicationgroup.Name,
Description = applicationgroup.Description
};
// load the roles/Roles for selection in the form:
foreach (var Role in allRoles)
{
var listItem = new SelectListItem()
{
Text = Role.Name,
Value = Role.Id,
Selected = groupRoles.Any(g => g.Id == Role.Id)
};
model.RolesList.Add(listItem);
}
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(
[Bind(Include = "Id,Name,Description")] GroupViewModel model,
params string[] selectedRoles)
{
var group = await this.GroupManager.FindByIdAsync(model.Id);
if (group == null)
{
return HttpNotFound();
}
if (ModelState.IsValid)
{
group.Name = model.Name;
group.Description = model.Description;
await this.GroupManager.UpdateGroupAsync(group);
selectedRoles = selectedRoles ?? new string[] { };
await this.GroupManager.SetGroupRolesAsync(group.Id, selectedRoles);
return RedirectToAction("Index");
}
return View(model);
}
public async Task<ActionResult> Delete(string id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
ApplicationGroup applicationgroup = await this.GroupManager.FindByIdAsync(id);
if (applicationgroup == null)
{
return HttpNotFound();
}
return View(applicationgroup);
}
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<ActionResult> DeleteConfirmed(string id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
ApplicationGroup applicationgroup = await this.GroupManager.FindByIdAsync(id);
await this.GroupManager.DeleteGroupAsync(id);
return RedirectToAction("Index");
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
}
正如我们所见,我们使用。类似于现有的
ApplicationGroupManager 来管理组的创建以及将角色分配给不同的组。现在,我们需要修改UsersAdminController
,以便我们不再直接将用户分配给角色,而是将用户分配给组,通过组来获得分配给每个组的角色权限。
修改 UsersAdminController
我们需要对UsersAdminController
UsersAdminController 进行一些调整,以反映我们管理组和角色的方式。如前所述,我们现在是将用户分配到组,而不是直接分配到角色,我们的UsersAdminController
UsersAdminController 需要反映这一点。
首先,我们需要向控制器中添加一个ApplicationGroupManager
ApplicationGroupManager 的实例。接下来,我们需要更新所有控制器方法,以使用组而不是角色。当我们创建一个新用户时,我们希望视图包含一个可用组的列表,用户可能会被分配到这些组。当我们编辑一个现有用户时,我们希望可以选择修改组的分配。当我们删除一个用户时,我们需要确保相应的组关系也被删除。
以下是整个UsersAdminController
UsersAdminController 的更新后代码。直接复制整个代码比逐段检查要容易。然后您可以大致浏览一下,就能很容易地理解每个控制器方法中发生了什么。
修改后的 UsersAdminController
[Authorize(Roles = "Admin")]
public class UsersAdminController : Controller
{
public UsersAdminController()
{
}
public UsersAdminController(ApplicationUserManager userManager,
ApplicationRoleManager roleManager)
{
UserManager = userManager;
RoleManager = roleManager;
}
private ApplicationUserManager _userManager;
public ApplicationUserManager UserManager
{
get
{
return _userManager ?? HttpContext.GetOwinContext()
.GetUserManager<ApplicationUserManager>();
}
private set
{
_userManager = value;
}
}
// Add the Group Manager (NOTE: only access through the public
// Property, not by the instance variable!)
private ApplicationGroupManager _groupManager;
public ApplicationGroupManager GroupManager
{
get
{
return _groupManager ?? new ApplicationGroupManager();
}
private set
{
_groupManager = value;
}
}
private ApplicationRoleManager _roleManager;
public ApplicationRoleManager RoleManager
{
get
{
return _roleManager ?? HttpContext.GetOwinContext()
.Get<ApplicationRoleManager>();
}
private set
{
_roleManager = value;
}
}
public async Task<ActionResult> Index()
{
return View(await UserManager.Users.ToListAsync());
}
public async Task<ActionResult> Details(string id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var user = await UserManager.FindByIdAsync(id);
// Show the groups the user belongs to:
var userGroups = await this.GroupManager.GetUserGroupsAsync(id);
ViewBag.GroupNames = userGroups.Select(u => u.Name).ToList();
return View(user);
}
public ActionResult Create()
{
// Show a list of available groups:
ViewBag.GroupsList =
new SelectList(this.GroupManager.Groups, "Id", "Name");
return View();
}
[HttpPost]
public async Task<ActionResult> Create(RegisterViewModel userViewModel,
params string[] selectedGroups)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser
{
UserName = userViewModel.Email,
Email = userViewModel.Email
};
var adminresult = await UserManager
.CreateAsync(user, userViewModel.Password);
//Add User to the selected Groups
if (adminresult.Succeeded)
{
if (selectedGroups != null)
{
selectedGroups = selectedGroups ?? new string[] { };
await this.GroupManager
.SetUserGroupsAsync(user.Id, selectedGroups);
}
return RedirectToAction("Index");
}
}
ViewBag.Groups = new SelectList(
await RoleManager.Roles.ToListAsync(), "Id", "Name");
return View();
}
public async Task<ActionResult> Edit(string id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var user = await UserManager.FindByIdAsync(id);
if (user == null)
{
return HttpNotFound();
}
// Display a list of available Groups:
var allGroups = this.GroupManager.Groups;
var userGroups = await this.GroupManager.GetUserGroupsAsync(id);
var model = new EditUserViewModel()
{
Id = user.Id,
Email = user.Email
};
foreach (var group in allGroups)
{
var listItem = new SelectListItem()
{
Text = group.Name,
Value = group.Id,
Selected = userGroups.Any(g => g.Id == group.Id)
};
model.GroupsList.Add(listItem);
}
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(
[Bind(Include = "Email,Id")] EditUserViewModel editUser,
params string[] selectedGroups)
{
if (ModelState.IsValid)
{
var user = await UserManager.FindByIdAsync(editUser.Id);
if (user == null)
{
return HttpNotFound();
}
// Update the User:
user.UserName = editUser.Email;
user.Email = editUser.Email;
await this.UserManager.UpdateAsync(user);
// Update the Groups:
selectedGroups = selectedGroups ?? new string[] { };
await this.GroupManager.SetUserGroupsAsync(user.Id, selectedGroups);
return RedirectToAction("Index");
}
ModelState.AddModelError("", "Something failed.");
return View();
}
public async Task<ActionResult> Delete(string id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var user = await UserManager.FindByIdAsync(id);
if (user == null)
{
return HttpNotFound();
}
return View(user);
}
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<ActionResult> DeleteConfirmed(string id)
{
if (ModelState.IsValid)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var user = await UserManager.FindByIdAsync(id);
if (user == null)
{
return HttpNotFound();
}
// Remove all the User Group references:
await this.GroupManager.ClearUserGroupsAsync(id);
// Then Delete the User:
var result = await UserManager.DeleteAsync(user);
if (!result.Succeeded)
{
ModelState.AddModelError("", result.Errors.First());
return View();
}
return RedirectToAction("Index");
}
return View();
}
}
既然我们所有的控制器都已就位,我们需要添加和/或更新一些视图。
在主布局视图中添加“组管理”作为菜单项
为了访问我们新的组功能,我们需要在 Views => Shared => _Layout.cshtml 中添加一个菜单项。由于我们只希望管理员用户访问该菜单项,我们想把它和其他管理类视图的链接放在一起。按如下方式修改 _Layout.cshtml 文件(我只包含了下面视图模板的相关部分):
在主布局视图中添加“组管理”链接
// Other View Code before...:
<ul class="nav navbar-nav">
<li>@Html.ActionLink("Home", "Index", "Home")</li>
<li>@Html.ActionLink("About", "About", "Home")</li>
<li>@Html.ActionLink("Contact", "Contact", "Home")</li>
@if (Request.IsAuthenticated && User.IsInRole("Admin"))
{
<li>@Html.ActionLink("RolesAdmin", "Index", "RolesAdmin")</li>
<li>@Html.ActionLink("UsersAdmin", "Index", "UsersAdmin")</li>
<li>@Html.ActionLink("GroupsAdmin", "Index", "GroupsAdmin")</li>
}
</ul>
@Html.Partial("_LoginPartial")
// ...Other View Code After...
为 GroupsAdminController 添加视图
我们将快速完成这一步,因为关于视图模板代码不需要太多讨论。显然,我们需要为 GroupsAdminController 的每个操作方法创建一个视图。
由于 Visual Studio 使用“添加视图”上下文菜单命令生成的内容不完全符合我们的需求,我们将手动完成此操作。
在 Views 目录下添加一个名为 GroupsAdmin 的文件夹。现在添加以下视图:
GroupsAdmin Index.cshtml 视图
@model IEnumerable<IdentitySample.Models.ApplicationGroup>
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
<table class="table">
<tr>
<th>
@Html.DisplayNameFor(model => model.Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Description)
</th>
<th></th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Description)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.Id }) |
@Html.ActionLink("Details", "Details", new { id=item.Id }) |
@Html.ActionLink("Delete", "Delete", new { id=item.Id })
</td>
</tr>
}
</table>
GroupsAdmin Create.cshtml 视图
@model IdentitySample.Models.ApplicationGroup
@{
ViewBag.Title = "Create";
}
<h2>Create</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>ApplicationGroup</h4>
<hr />
@Html.ValidationSummary(true)
<div class="form-group">
@Html.LabelFor(model => model.Name, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Description, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Description)
@Html.ValidationMessageFor(model => model.Description)
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label">
Select Group Roles
</label>
<div class="col-md-10">
@foreach (var item in (SelectList)ViewBag.RolesList)
{
<div>
<input type="checkbox" name="SelectedRoles" value="@item.Text" class="checkbox-inline" />
@Html.Label(item.Text, new { @class = "control-label" })
</div>
}
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
GroupsAdmin Edit.cshtml 视图
@model IdentitySample.Models.GroupViewModel
@{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>ApplicationGroup</h4>
<hr />
@Html.ValidationSummary(true)
@Html.HiddenFor(model => model.Id)
<div class="form-group">
@Html.LabelFor(model => model.Name, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Description, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Description)
@Html.ValidationMessageFor(model => model.Description)
</div>
</div>
<div class="form-group">
@Html.Label("Permissions", new { @class = "control-label col-md-2" })
<span class=" col-md-10">
@foreach (var item in Model.RolesList)
{
<div>
<input type="checkbox" name="selectedRoles" value="@item.Text" checked="@item.Selected" class="checkbox-inline" />
@Html.Label(item.Text, new { @class = "control-label" })
</div>
}
</span>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
GroupsAdmin Details.cshtml 视图
@model IdentitySample.Models.ApplicationGroup
@{
ViewBag.Title = "Details";
}
<h2>Details</h2>
<div>
<h4>ApplicationGroup</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd>
@Html.DisplayFor(model => model.Name)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Description)
</dt>
<dd>
@Html.DisplayFor(model => model.Description)
</dd>
</dl>
</div>
<h4>List of permissions granted this group</h4>
@if (ViewBag.PermissionsCount == 0)
{
<hr />
<p>No users found in this role.</p>
}
<table class="table">
@foreach (var item in ViewBag.RolesList)
{
<tr>
<td>
@item
</td>
</tr>
}
</table>
<p>
@Html.ActionLink("Edit", "Edit", new { id = Model.Id }) |
@Html.ActionLink("Back to List", "Index")
</p>
GroupsAdmin Delete.cshtml 视图
@model IdentitySample.Models.ApplicationGroup
@{
ViewBag.Title = "Delete";
}
<h2>Delete</h2>
<h3>Are you sure you want to delete this?</h3>
<div>
<h4>ApplicationGroup</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd>
@Html.DisplayFor(model => model.Name)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Description)
</dt>
<dd>
@Html.DisplayFor(model => model.Description)
</dd>
</dl>
@using (Html.BeginForm()) {
@Html.AntiForgeryToken()
<div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-default" /> |
@Html.ActionLink("Back to List", "Index")
</div>
}
</div>
更新 UserAdmin 视图
我们需要对 UserAdmin 视图做一些微小的改动。在现有项目中,UserAdmin 的创建和编辑视图允许我们为用户分配角色。现在,我们希望将用户分配到一个或多个组。按如下方式更新创建和编辑视图:
修改视图代码时请密切注意,Viewbag 属性的名称很重要。
修改后的 UserAdmin Create.cshtml 视图
@model IdentitySample.Models.RegisterViewModel
@{
ViewBag.Title = "Create";
}
<h2>@ViewBag.Title.</h2>
@using (Html.BeginForm("Create", "UsersAdmin", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
@Html.AntiForgeryToken()
<h4>Create a new account.</h4>
<hr />
@Html.ValidationSummary("", new { @class = "text-error" })
<div class="form-group">
@Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.PasswordFor(m => m.Password, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label">
Select User Groups
</label>
<div class="col-md-10">
@foreach (var item in (SelectList)ViewBag.GroupsList)
{
<div>
<input type="checkbox" name="selectedGroups" value="@item.Value" class="checkbox-inline" />
@Html.Label(item.Text, new { @class = "control-label" })
</div>
}
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" class="btn btn-default" value="Create" />
</div>
</div>
}
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
高亮区域指出了主要受影响的代码。
接下来,我们将类似地修改 UsersAdmin => Edit.cshtml 文件。
修改后的 UsersAdmin Edit.cshtml 视图
@model IdentitySample.Models.EditUserViewModel
@{
ViewBag.Title = "Edit";
}
<h2>Edit.</h2>
@using (Html.BeginForm()) {
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Edit User Form.</h4>
<hr />
@Html.ValidationSummary(true)
@Html.HiddenFor(model => model.Id)
<div class="form-group">
@Html.LabelFor(model => model.Email, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.Email)
</div>
</div>
<div class="form-group">
@Html.Label("This User belongs to the following Groups", new { @class = "control-label col-md-2" })
<span class=" col-md-10">
@foreach (var item in Model.GroupsList)
{
<div>
<input type="checkbox" name="selectedGroups" value="@item.Value" checked="@item.Selected" class="checkbox-inline" />
@Html.Label(item.Text, new { @class = "control-label" })
</div>
}
</span>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
UsersAdmin Details.cshtml 视图显示了分配给当前所选用户的角色列表。我们将改为显示组列表。
修改后的 UsersAdmin Details.cshtml 视图
@model IdentitySample.Models.ApplicationUser
@{
ViewBag.Title = "Details";
}
<h2>Details.</h2>
<div>
<h4>User</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.UserName)
</dt>
<dd>
@Html.DisplayFor(model => model.UserName)
</dd>
</dl>
</div>
<h4>List ofGroups this user belongs to:</h4>
@if (ViewBag.GroupNames.Count == 0)
{
<hr />
<p>No Groups found for this user.</p>
}
<table class="table">
@foreach (var item in ViewBag.GroupNames)
{
<tr>
<td>
@item
</td>
</tr>
}
</table>
<p>
@Html.ActionLink("Edit", "Edit", new { id = Model.Id }) |
@Html.ActionLink("Back to List", "Index")
</p>
当然,这个项目中的视图还有很多可以改进的地方,例如,我们可以添加更多视图来显示用户的有效权限(用户因其所属组的成员身份而拥有的所有权限的总和)。不过,现在我们先让功能跑起来,然后您可以根据自己项目的需要进行微调。
修改 Identity.Config 文件和 DbInitializer
在之前的项目中,当我们在 Identity 1.0 下设置基于组的权限管理时,我们使用了 EF Migrations 来执行数据库创建和 Code-First 生成。这一次,我们将继续使用来自 IdentitySamples 项目的 DbInitializer。
该ApplicationDbInitializer
DbInitializer 在 App_Start => IdentityConfig.cs 中定义。为了开发目的,我已将其设置为继承自DropCreateDatabaseAlways
DropCreateDatabaseAlways。但是,您可以轻松地将其更改为DropCreateDatabaseIfModelChanges
.
。当然,我们希望我们的应用程序在运行时有一个基本的配置。目前,DbInitializer 通过创建一个默认用户、一个管理员角色,然后将默认用户分配给该角色来设置。
我们想要创建相同的默认用户,但随后还要创建一个默认组。然后,我们将创建默认的管理员角色并将其分配给默认组。最后,我们将用户添加到该组。
打开 App_Start => Identity.Config 文件,并对ApplicationDbInitializer 中的 InitializeIdentityForEF()
方法进行以下更改:ApplicationDbInitializer
类
InitializeIdentityForEF 方法
public static void InitializeIdentityForEF(ApplicationDbContext db) {
var userManager = HttpContext.Current
.GetOwinContext().GetUserManager<ApplicationUserManager>();
var roleManager = HttpContext.Current
.GetOwinContext().Get<ApplicationRoleManager>();
const string name = "admin@example.com";
const string password = "Admin@123456";
const string roleName = "Admin";
//Create Role Admin if it does not exist
var role = roleManager.FindByName(roleName);
if (role == null) {
role = new ApplicationRole(roleName);
var roleresult = roleManager.Create(role);
}
var user = userManager.FindByName(name);
if (user == null) {
user = new ApplicationUser
{
UserName = name,
Email = name,
EmailConfirmed = true
};
var result = userManager.Create(user, password);
result = userManager.SetLockoutEnabled(user.Id, false);
}
var groupManager = new ApplicationGroupManager();
var newGroup = new ApplicationGroup("SuperAdmins", "Full Access to All");
groupManager.CreateGroup(newGroup);
groupManager.SetUserGroups(user.Id, new string[] { newGroup.Id });
groupManager.SetGroupRoles(newGroup.Id, new string[] { role.Name });
}
至此,我们应该可以运行应用程序了。
运行应用程序
一旦我们启动了应用程序并登录,我们应该能够浏览各种管理功能。为了让事情更有趣,让我们添加一些角色、组和用户,看看效果如何:
如果我们添加了一些额外的角色、用户和组,我们就能开始看到这在真实世界的应用程序中是如何工作的。让我们以两个虚构的部门——销售部和采购部为例。我们可能会有一些相对细粒度的角色(或许,在我们的基本控制器操作层面)对应每个职能:
销售部和采购部的基本角色
现在,这个视图以及我们其他的视图很可能需要设计师的帮助,或者至少需要一些能提供更好分组的表示控件。尽管如此,我们可以看到我们在这里添加了一些角色,这些角色大致对应于我们可能在CustomersController
和和 ProductsController
上找到的假设的控制器操作(好吧——为了这个例子,我们简化了一点,但你应该明白我的意思)。
现在,我们可能还会定义一些组:
销售部和采购部的基本组
同样,如果我们关心设计美学,我们可能会觉得我们的组管理视图有点欠缺。但你可以看到,我们已经定义了一些与销售和采购部门职能相关的组。
现在,如果我们编辑其中一个组,我们可以为该组分配适当的角色:
编辑组时分配组角色
在这里,我们决定 SalesAdmin 组中的用户应该能够添加/编辑/查看/删除客户数据,以及查看(但不能修改)产品数据。
再次强调,我们可以在这里对可用角色的显示和分组做一些改进,但这对于演示目的已经足够了。
现在,如果我们像这样保存 SalesAdmin 组,我们就可以分配一个或多个用户,这些用户将获得与此组相关的所有权限。
创建具有组分配的新用户
一旦我们保存,Jim 将成为 SalesAdmin 组的成员,并将拥有我们分配给该组的所有角色权限。
使用 [Authorize] 特性控制访问
当然,如果我们不将这些细粒度的角色权限付诸实践,这一切都毫无用处。
首先,我们可能需要对我们的。类似于现有的
GroupsAdminController 本身添加一些访问控制。我们可能想为整个控制器添加一个[Authorize]
[Authorize(Roles="Admin")] 的装饰,类似于和 RolesAdminController
UsersAdminController,这样只有拥有 Admin 角色的用户才能修改组。
除此之外,让我们扩展一下上面的例子。考虑一个假设的CustomersController
CustomersController。我们可以按照我们定义的角色来装饰基本的控制器方法,如下所示:
假设的 CustomersController
public class CustomerController
{
[Authorize(Roles = "CanViewCustomers")]
public async ActionResult Index()
{
// Code...
}
[Authorize(Roles = "CanAddCustomers")]
public async ActionResult Create()
{
// Code...
}
[HttpPost]
[ValidateAntiForgeryToken]
[Authorize(Roles = "CanAddCustomers")]
public async Task<ActionResult> Create(SomeArguments)
{
// Code...
}
[Authorize(Roles = "CanEditCustomers")]
public async ActionResult Edit(int id)
{
// Code...
}
[HttpPost]
[ValidateAntiForgeryToken]
[Authorize(Roles = "CanEditCustomers")]
public async Task<ActionResult> Edit(SomeArguments)
{
// Code...
}
[Authorize(Roles = "CanDeleteCustomers")]
public async Task<ActionResult> Delete(id)
{
// Code...
}
[HttpPost]
[ValidateAntiForgeryToken]
[Authorize(Roles = "CanDeleteCustomers")]
public async Task<ActionResult> Delete(SomeArguments)
{
// Code...
}
}
我们可以看到,我们现在已经实现了一些与我们定义的某些角色相对应的授权控制。这些角色本身并不特定于任何一种类型的用户。相反,角色可以被分配给不同的组。从那里开始,根据用户的功能和访问需求,添加或删除用户到一个或多个组就变得很简单了。
关于授权和安全的一些思考
这个项目中的概念可以看作是 Identity 2.0 开箱即用提供的基本但功能强大的授权机制与使用声明(Claims)或活动目录(Active Directory)等更复杂实现之间的一种中间地带。
这里展示的系统将提供对角色和授权的更精细的控制,以访问和执行代码。然而,这有一个实际的限制。
设计一个健壮的应用程序授权矩阵需要仔细思考,并选择正确的工具(就像开发中的大多数事情一样)。预先仔细规划将极大地帮助您创建一个坚实、可维护的应用程序。
如果你的授权和角色定义过于细粒度,你将会得到一个难以管理的混乱局面。如果不够细粒度,则会导致一个笨拙、受限的授权方案,你可能会发现自己给予用户的访问权限过多或过少。
勘误、想法和拉取请求
在为本文整理代码时,我完全有可能遗漏了某些东西。此外,如前所述,这里的设计还有很大的改进空间。如果您发现任何问题,有改进这个概念的想法,或者(特别是)希望在组织角色、组等方面改进视图,请提交一个 issue 或给我发送一个 Pull Request。
其他资源和感兴趣的项目
- 此项目的完整源代码在 Github 上
- 构建此项目的可扩展模板的源代码
- ASP.NET MVC 和 Identity 2.0:了解基础知识
- ASP.NET Identity 2.0:自定义用户和角色
- 原始 Identity 1.0 项目:实现基于组的权限管理
- ASP.NET Identity 2.0 扩展身份模型并使用整数键而不是字符串
- ASP.NET MVC 中的路由基础
- ASP.NET MVC 中的自定义路由