65.9K
CodeProject 正在变化。 阅读更多。
Home

扩展 ASP.NET MVC 5 中的身份账户并实现基于角色的认证

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (80投票s)

2013年11月13日

CPOL

28分钟阅读

viewsIcon

452374

在 ASP.NET MVC 5 中实现简单的基于角色的身份管理和使用身份账户。

5555555-500在上一篇文章中,我们快速了解了在 ASP.NET MVC 5 新身份系统背景下扩展身份账户。我们还研究了 Entity Framework Code-First Migrations 的基础知识。如果您还没有阅读那篇文章,您可能想现在阅读,这样我们就不用重复解释其中的一些一般概念了。

图片来源: Elif Ayiter | 保留部分权利

注意:本文最初发布在我的个人博客http://typecastexception.com上。我的 RSS 订阅存在问题(仍在处理中),因此 CD 技术博客阅读器未自动抓取。因此,我手动将其作为文章迁移到这里。内容是我自己的,没有以任何方式抄袭。

如果您正在使用 Identity 2.0 框架

本文重点介绍 ASP.NET Identity 框架的版本 1.0 的自定义和修改。如果您正在使用最近发布的 2.0 版本,本文中的代码将无法运行。有关使用 Identity 2.0 的更多信息,请参阅ASP.NET Identity 2.0:了解基础知识ASP.NET Identity 2.0:设置账户验证和双因素认证

本文中实现的许多自定义功能都包含在 Identity Samples 项目中。我将在新文章ASP.NET Identity 2.0:自定义用户和角色中讨论在 Identity 2.0 中扩展和自定义 IdentityUser 和 IdentityRole。

如果您正在使用 Identity 1.0 框架

出于本文的目的,我们将着眼于为 ASP.NET MVC 5 Web 应用程序实现相对简单的基于角色的认证和身份管理。所使用的示例将刻意简化,虽然它们将有效地说明设置基于角色的身份管理的基础知识,但我可以保证这里的实现将缺少我们在生产项目中希望看到的一些东西(例如完整的异常处理)。此外,生产应用程序建模可能会根据业务需求略有不同。

换句话说,我将保持简单,就像我们高中物理课上忽略摩擦力效应一样。要了解如何实现更精细的应用程序权限管理,请参阅 ASP.NET MVC 5 Identity:实现基于组的权限管理

话虽如此,还有很多内容要介绍。这篇文章并没有看起来那么长,因为我包含了一些大型代码示例和图片来说明正在发生的事情。

下载源代码

2014年1月28日 - 重要提示:根据以下一些评论,我强烈建议从 Github 克隆本文的源代码。这里有很多活动部件,很容易错过一些关键步骤。

本文的完整项目源代码可在我的 Github 仓库中找到。注意:您需要启用 Nuget 包还原才能正确构建项目。

基本假设的应用程序需求

我们将假设我们的身份管理需要为主要由内部用户或经管理层适当授权的其他用户使用的业务线 Web 应用程序执行以下操作。该应用程序将具有最小的公共界面,并且所有用户都需要登录才能访问即使是最小的功能(很容易扩展我们正在做的事情以包含公共用户,但这源于我需要在工作中快速创建的实际应用程序)。

  • 用户账户必须由一个或多个管理员级别用户创建。“注册”在我们从 ASP.NET Membership 范例中了解的那样,不存在(匿名用户无法从公共站点注册和创建账户)。
  • 用户身份账户将扩展为包含电子邮件地址、名字和姓氏
  • 出于我们的目的,将至少有三个角色;管理员(完全访问所有内容)、编辑(可以执行应用程序的大部分业务功能,但不能访问账户管理等管理功能)和只读用户(顾名思义)。
  • 每个用户可以是零个或多个角色的成员。多个角色将拥有所有角色组合的访问权限。
  • 为简单起见,角色是独立定义的,并且不是其他角色的成员。
  • 所有应用程序角色都是预定义的。没有角色的管理创建。此外,角色权限是应用程序不可或缺的一部分,并且无法由管理员管理(这在此刻是为了简化)。
  • 不允许匿名访问站点,登录门户除外。
  • 将不使用外部登录或 OAuth(此代码作为默认 MVC 项目的一部分包含;我们将删除它以保持整洁)。

在上面,我故意省略了角色的管理创建/删除。为了我们的例子,这增加了一个无法接受的复杂性。因为在这种情况下,我们的访问权限将使用 [Authorize] 属性(换句话说,使用硬编码值)进行管理,所以添加或删除角色将创建超出我们示例范围的问题。有办法解决这个问题,我将在未来的文章中讨论。

 

 

入门 – 从 MVC 项目模板中删除不需要的项

虽然我们可以从上一篇关于扩展身份账户的文章中创建的内容开始,但我们将充分重新排列事物,以便我们从头开始会更简洁。在 Visual Studio 中创建一个新的 ASP.NET MVC 项目。在我们做任何其他事情之前,让我们清除一些不需要的杂乱,以便只剩下我们需要的东西。

我们将删除大量与管理外部登录相关的代码,并清除默认项目附带的一些多余注释(对我来说,这些注释只会增加代码的噪音)。

我们将从 AccountController 开始。

简化 AccountController – 清理杂乱

AccountController 中有许多与外部登录相关的方法,我们不需要它们。如果您仔细检查 AccountController,您可以遍历并删除以下方法的代码

  • Dissociate()
  • ExternalLogin()
  • ExternalLoginCallback()
  • LinkLogin()
  • LinkLoginCallback()
  • ExternalLoginConfirmation()
  • ExternalLoginFailure()
  • RemoveAccountList()

此外,如果您仔细查看,有一个用于助手的代码 #region(我知道。我讨厌 #region,并认为它应该被淘汰)。从这里,我们可以删除以下项目

  • 成员常量 XsrfKey
  • 整个类 ChallengeResult

此时,我们还可以右键单击代码文件并选择移除并排序 Usings,以清除此处一些不需要的杂乱。

此时,我们的 AccountController.cs 文件应包含以下方法(为简洁起见,此处仅为简单存根 - 我们将很快讨论代码)

清理后的 AccountController.cs 文件(仅存根 - 为简洁起见,代码已隐藏)
using AspNetRoleBasedSecurity.Models;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.Owin.Security;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
  
namespace AspNetRoleBasedSecurity.Controllers
{
    [Authorize]
    public class AccountController : Controller
    {
        public AccountController()
            : this(new UserManager<ApplicationUser>(
            new UserStore<ApplicationUser>(new ApplicationDbContext())))
        {
        }
  
  
        public AccountController(UserManager<ApplicationUser> userManager)
        {
            UserManager = userManager;
        }
  
  
        public UserManager<ApplicationUser> UserManager { get; private set; }
  
  
        [AllowAnonymous]
        public ActionResult Login(string returnUrl)
        {
            ViewBag.ReturnUrl = returnUrl;
            return View();
        }
  
  
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
        {
             // . . . Code Hidden for brevity
        }
  
  
        [AllowAnonymous]
        public ActionResult Register()
        {
            return View();
        }
  
  
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Register(RegisterViewModel model)
        {
             // . . . Code Hidden for brevity
        }
  
  
        public ActionResult Manage(ManageMessageId? message)
        {
             // . . . Code Hidden for brevity
        }
  
  
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Manage(ManageUserViewModel model)
        {
             // . . . Code Hidden for brevity
        }
  
  
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult LogOff()
        {
            AuthenticationManager.SignOut();
            return RedirectToAction("Index", "Home");
        }
   
  
        protected override void Dispose(bool disposing)
        {
             // . . . Code Hidden for brevity
        }
  
  
  
  
        #region Helpers
  
  
        private IAuthenticationManager AuthenticationManager
        {
             // . . . Code Hidden for brevity
        }
  
  
        private async Task SignInAsync(ApplicationUser user, bool isPersistent)
        {
             // . . . Code Hidden for brevity
        }
  
  
        private void AddErrors(IdentityResult result)
        {
             // . . . Code Hidden for brevity
        }
  
  
        private bool HasPassword()
        {
             // . . . Code Hidden for brevity
        }
  
  
        public enum ManageMessageId
        {
             // . . . Code Hidden for brevity
        }
  
  
        private ActionResult RedirectToLocal(string returnUrl)
        {
             // . . . Code Hidden for brevity
        }
  
  
        #endregion
    }
}

删除不需要的视图

除了我们刚刚删除的不必要的 Controller 方法外,我们还可以删除与外部登录相关的不必要的视图。如果我们在解决方案资源管理器中打开Views => Account文件夹,我们会发现可以从项目中删除下面突出显示的视图

解决方案资源管理器 – 删除不需要的视图

solution-explorer-remove-unneeded-views

现在已经移除了完全不需要的视图,让我们也从剩余的视图中移除外部登录代码。

清理与账户相关的视图

我们的账户相关视图中还有一些残留的杂物需要清除。我们不希望网站上出现死链接,并且我们希望视图中只保留相关的代码。

我们可以从 Login.cshtml 文件开始,其中包含一个与从各种社交网络创建外部登录相关的部分(用黄色突出显示)。

Login.cshtml - 移除社交网络登录选项
@{
    ViewBag.Title = "Log in";
}

<h2>@ViewBag.Title.</h2>
<div class="row">
    <div class="col-md-8">
        <section id="loginForm">
            @using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
            {
                @Html.AntiForgeryToken()
                <h4>Use a local account to log in.</h4>
                <hr />
                @Html.ValidationSummary(true)
                <div class="form-group">
                    @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" })
                    <div class="col-md-10">
                        @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" })
                        @Html.ValidationMessageFor(m => m.UserName)
                    </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" })
                        @Html.ValidationMessageFor(m => m.Password)
                    </div>
                </div>
                <div class="form-group">
                    <div class="col-md-offset-2 col-md-10">
                        <div class="checkbox">
                            @Html.CheckBoxFor(m => m.RememberMe)
                            @Html.LabelFor(m => m.RememberMe)
                        </div>
                    </div>
                </div>
                <div class="form-group">
                    <div class="col-md-offset-2 col-md-10">
                        <input type="submit" value="Log in" class="btn btn-default" />
                    </div>
                </div>
                <p>
                    @Html.ActionLink("Register", "Register") if you don't have a local account.
                </p>
            }
        </section>
    </div>
    <div class="col-md-4">
        <section id="socialLoginForm">
            @Html.Partial("_ExternalLoginsListPartial", new { Action = "ExternalLogin", ReturnUrl = ViewBag.ReturnUrl })
        </section>
    </div>
</div>
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

在上面,包含 <section> "socialLoginForm" 的整个 <div>(靠近底部)可以删除。

接下来,让我们从 Manage.cshtml 文件中删除类似的外部登录功能(再次用黄色突出显示)

Manage.cshtml – 移除外部登录项
@using AspNetRoleBasedSecurity.Models;
@using Microsoft.AspNet.Identity;
@{
    ViewBag.Title = "Manage Account";
}
  
<h2>@ViewBag.Title.</h2>
  
<p class="text-success">@ViewBag.StatusMessage</p>
<div class="row">
    <div class="col-md-12">
        @if (ViewBag.HasLocalPassword)
        {
            @Html.Partial("_ChangePasswordPartial")
        }
        else
        {
            @Html.Partial("_SetPasswordPartial")
        }
  
        <section id="externalLogins">
            @Html.Action("RemoveAccountList")
            @Html.Partial("_ExternalLoginsListPartial", new { Action = "LinkLogin", ReturnUrl = ViewBag.ReturnUrl })
        </section>
    </div>
</div>
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

和以前一样,带 id="externalLogins" 的 <section>(同样,靠近底部)可以安全移除。

删除不需要的模型类

与前面的部分一样,有一些不需要的模型类可以安全地删除,以便清理我们的项目。如果我们在解决方案资源管理器中展开Models文件夹,我们会发现有一个代码文件 AccountViewModels.cs,其中包含几个与身份管理相关的 ViewModel 类。仔细查看该文件,并删除下面突出显示的 ExternalLogInConfirmationViewModel

账户视图模型文件 – 删除不需要的类
using System.ComponentModel.DataAnnotations;

namespace AspNetRoleBasedSecurity.Models
{
    public class ExternalLoginConfirmationViewModel
    {
        [Required]
        [Display(Name = "User name")]
        public string UserName { get; set; }
    }
  
    public class ManageUserViewModel
    {
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Current password")]
        public string OldPassword { get; set; }
  
        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "New password")]
        public string NewPassword { get; set; }
  
        [DataType(DataType.Password)]
        [Display(Name = "Confirm new password")]
        [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
    }
  
    public class LoginViewModel
    {
        [Required]
        [Display(Name = "User name")]
        public string UserName { get; set; }

        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }

        [Display(Name = "Remember me?")]
        public bool RememberMe { get; set; }
    }
  
    public class RegisterViewModel
    {
        [Required]
        [Display(Name = "User name")]
        public string UserName { get; set; }
  
        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }
  
        [DataType(DataType.Password)]
        [Display(Name = "Confirm password")]
        [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
    }
}

扩展身份管理模型和视图模型

正如我们所见,我们的应用程序用于管理身份和授权的模型类包含在 IdentityModels.cs 文件中。此外,AccountViewModels.cs 文件中定义了与身份相关的 ViewModel 类,用于管理视图和控制器之间身份数据的传输。

这里需要注意的是,在我们运行应用程序并创建任何新的用户账户,或使用正常的(即将被删除的)MVC“注册”机制进行注册之前,我们确实希望所有模型定义都是正确的。我们将使用 Entity Framework Migrations 和 Code-First 为我们完成数据库的繁重工作。虽然以后更新我们的模型(并通过 EF 迁移更新数据库)并不特别困难,但从一开始就正确地处理它会更简洁、更顺畅。

为了符合我们的项目规范,我们需要做的第一件事是扩展默认的 ApplicationUser 类,使其包含 EmailLastNameFirstName 属性。打开 IdentityModels.cs 文件。目前,代码应该如下所示

带有空 ApplicationUser 存根的默认 IdentityModels 文件
using Microsoft.AspNet.Identity.EntityFramework;
  
namespace AspNetRoleBasedSecurity.Models
{
    public class ApplicationUser : IdentityUser
    {
    }
  
  
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext()
            : base("DefaultConnection")
        {
        }
    }
}

在此步骤中,我们将扩展 ApplicationUser 类以包含我们的应用程序规范所需的属性。此外,我们将添加一个 IdentityManager 类,在该类中我们将整合我们的用户和角色管理功能。我们稍后将讨论所有这些如何工作。现在,将以下代码添加到 IdentityModels.cs 代码文件(请注意,我们还在顶部添加了一些新的 using 语句)

修改后的 IdentityModels.cs 文件
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
  
namespace AspNetRoleBasedSecurity.Models
{
    public class ApplicationUser : IdentityUser
    {
        [Required]
        public string FirstName { get; set; }
   
        [Required]
        public string LastName { get; set; }
   
        [Required]
        public string Email { get; set; }
    }
    
  
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext()
            : base("DefaultConnection")
        {
            
        }
    }
  
  
    public class IdentityManager
    {
        public bool RoleExists(string name)
        {
            var rm = new RoleManager<IdentityRole>(
                new RoleStore<IdentityRole>(new ApplicationDbContext()));
            return rm.RoleExists(name);
        }
  
  
        public bool CreateRole(string name)
        {
            var rm = new RoleManager<IdentityRole>(
                new RoleStore<IdentityRole>(new ApplicationDbContext()));
            var idResult = rm.Create(new IdentityRole(name));
            return idResult.Succeeded;
        }
  
  
        public bool CreateUser(ApplicationUser user, string password)
        {
            var um = new UserManager<ApplicationUser>(
                new UserStore<ApplicationUser>(new ApplicationDbContext()));
            var idResult = um.Create(user, password);
            return idResult.Succeeded;
        }
  
  
        public bool AddUserToRole(string userId, string roleName)
        {
            var um = new UserManager<ApplicationUser>(
                new UserStore<ApplicationUser>(new ApplicationDbContext()));
            var idResult = um.AddToRole(userId, roleName);
            return idResult.Succeeded;
        }
  
  
        public void ClearUserRoles(string userId)
        {
            var um = new UserManager<ApplicationUser>(
                new UserStore<ApplicationUser>(new ApplicationDbContext()));
            var user = um.FindById(userId);
            var currentRoles = new List<IdentityUserRole>();
            currentRoles.AddRange(user.Roles);
            foreach(var role in currentRoles)
            {
                um.RemoveFromRole(userId, role.Role.Name);
            }
        }
    }
}

是的,我知道。这里还有一些重构的空间。我们暂时忽略它。在上面,我们已经扩展了 ApplicationUser 以包含我们新所需的属性,并添加了 IdentityManager 类,其中包括创建新用户以及从可用角色中添加/删除用户所需的方法。

我们还为新的 ApplicationUser 属性添加了 [Required] 数据注解,这将在我们的数据库中(不允许空值)和我们视图的模型验证中得到体现。

旁注,重申:我正在直接使用 Microsoft.AspNet.IdentityMicrosoft.AspNet.Identity.EntityFramework 命名空间中可用的方法。我乐于让 ASP.NET 团队发明并提供管理应用程序安全的最佳实践。因此,在这个应用程序的上下文中,我没有发明自己的。我强烈建议你也这样做。很容易发现管理某些账户/身份(包括数据持久性)的方法看起来更直接或更容易。我得出的结论是,团队比我更有效地考虑了所有这些。因此,虽然我们在这里创建了一个授权管理结构,但我们是使用那些比我们更了解的人提供的核心实现来做的。

扩展账户管理视图模型

现在我们已经扩展了基本的身份模型,我们需要对账户视图模型做同样的事情。视图模型本质上代表了我们的视图和控制器之间的数据交换机制。我们在这里的目标是为我们的账户管理视图提供执行手头任务所需的确切信息,不多也不少。

另请注意的是,为了我们的表示层,我没有将用户或角色 ID 推送到页面或后端 HTML 上。相反,我依赖于 UsernameRole 名称的唯一性来在服务器端查找正确的 id 值。

目前,在我们做任何事情之前,我们的 AccountViewModels.cs 文件看起来是这样的

修改前的 AccountViewModels.cs 文件
using System.ComponentModel.DataAnnotations;

namespace AspNetRoleBasedSecurity.Models
{
    public class ManageUserViewModel
    {
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Current password")]
        public string OldPassword { get; set; }
  
        [Required]
        [StringLength(100, ErrorMessage = 
            "The {0} must be at least {2} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "New password")]
        public string NewPassword { get; set; }
  
        [DataType(DataType.Password)]
        [Display(Name = "Confirm new password")]
        [Compare("NewPassword", ErrorMessage = 
            "The new password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
    }
  
    public class LoginViewModel
    {
        [Required]
        [Display(Name = "User name")]
        public string UserName { get; set; }
  
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }
  
        [Display(Name = "Remember me?")]
        public bool RememberMe { get; set; }
    }
  
    public class RegisterViewModel
    {
        [Required]
        [Display(Name = "User name")]
        public string UserName { get; set; }
  
        [Required]
        [StringLength(100, ErrorMessage = 
            "The {0} must be at least {2} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }
  
        [DataType(DataType.Password)]
        [Display(Name = "Confirm password")]
        [Compare("Password", ErrorMessage = 
            "The password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
    }
}

我们将对其进行显著扩展。事实上,这可能看起来是多余的。因为,除了少数细微差别,以下代码中的一个或两个 ViewModel 可能看起来几乎是重复的,并且可能适合重构为单个类。但是,我决定 ViewModel 的目的是表示特定视图所需的特定数据。虽然在某些情况下它们看起来是相同的,但这可能会改变。我得出的结论是,就视图和视图模型而言,最好有一些潜在的重复,但要保留每个视图独立演变的能力,以防需要,而无需担心对依赖相同 ViewModel 的其他视图的影响。

按如下方式修改上述代码(或直接用以下内容替换)。我们稍后将在控制器实现中更仔细地查看其功能

AccountViewModels.cs 文件的修改代码
using System.ComponentModel.DataAnnotations;
  
// New namespace imports:
using Microsoft.AspNet.Identity.EntityFramework;
using System.Collections.Generic;
  
namespace AspNetRoleBasedSecurity.Models
{
    public class ManageUserViewModel
    {
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Current password")]
        public string OldPassword { get; set; }
  
        [Required]
        [StringLength(100, ErrorMessage = 
            "The {0} must be at least {2} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "New password")]
        public string NewPassword { get; set; }
  
        [DataType(DataType.Password)]
        [Display(Name = "Confirm new password")]
        [Compare("NewPassword", ErrorMessage = 
            "The new password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
    }
  
  
    public class LoginViewModel
    {
        [Required]
        [Display(Name = "User name")]
        public string UserName { get; set; }
  
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }
  
        [Display(Name = "Remember me?")]
        public bool RememberMe { get; set; }
    }
  
  
    public class RegisterViewModel
    {
        [Required]
        [Display(Name = "User name")]
        public string UserName { get; set; }
  
        [Required]
        [StringLength(100, ErrorMessage = 
            "The {0} must be at least {2} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }
  
        [DataType(DataType.Password)]
        [Display(Name = "Confirm password")]
        [Compare("Password", ErrorMessage = 
            "The password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
  
        // New Fields added to extend Application User class:
  
        [Required]
        [Display(Name = "First Name")]
        public string FirstName { get; set; }
  
        [Required]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
  
        [Required]
        public string Email { get; set; }
  
        // Return a pre-poulated instance of AppliationUser:
        public ApplicationUser GetUser()
        {
            var user = new ApplicationUser()
            {
                UserName = this.UserName,
                FirstName = this.FirstName,
                LastName = this.LastName,
                Email = this.Email,
            };
            return user;
        }
    }
  
  
    public class EditUserViewModel
    {
        public EditUserViewModel() { }
  
        // Allow Initialization with an instance of ApplicationUser:
        public EditUserViewModel(ApplicationUser user)
        {
            this.UserName = user.UserName;
            this.FirstName = user.FirstName;
            this.LastName = user.LastName;
            this.Email = user.Email;
        }
  
        [Required]
        [Display(Name = "User Name")]
        public string UserName { get; set; }
  
        [Required]
        [Display(Name = "First Name")]
        public string FirstName { get; set; }
  
        [Required]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
  
        [Required]
        public string Email { get; set; }
    }
  
  
    public class SelectUserRolesViewModel
    {
        public SelectUserRolesViewModel() 
        {
            this.Roles = new List<SelectRoleEditorViewModel>();
        }
  
  
        // Enable initialization with an instance of ApplicationUser:
        public SelectUserRolesViewModel(ApplicationUser user) : this()
        {
            this.UserName = user.UserName;
            this.FirstName = user.FirstName;
            this.LastName = user.LastName;
  
            var Db = new ApplicationDbContext();
  
            // Add all available roles to the list of EditorViewModels:
            var allRoles = Db.Roles;
            foreach(var role in allRoles)
            {
                // An EditorViewModel will be used by Editor Template:
                var rvm = new SelectRoleEditorViewModel(role);
                this.Roles.Add(rvm);
            }
  
            // Set the Selected property to true for those roles for 
            // which the current user is a member:
            foreach(var userRole in user.Roles)
            {
                var checkUserRole = 
                    this.Roles.Find(r => r.RoleName == userRole.Role.Name);
                checkUserRole.Selected = true;
            }
        }
  
        public string UserName { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public List<SelectRoleEditorViewModel> Roles { get; set; }
    }
  
    // Used to display a single role with a checkbox, within a list structure:
    public class SelectRoleEditorViewModel
    {
        public SelectRoleEditorViewModel() {}
        public SelectRoleEditorViewModel(IdentityRole role)
        {
            this.RoleName = role.Name;
        }
  
        public bool Selected { get; set; }
  
        [Required]
        public string RoleName { get; set;}
    }
}

扩展账户控制器

现在我们的模型和视图模型大部分都已到位,让我们看看它们如何在控制器中协同工作。我们当前的 AccountController 定义了以下控制器操作

  • Register(本质上是创建一个新用户)
  • Manage(本质上允许用户更改密码)
  • 登录
  • LogOff

此外,上述方法主要关注允许匿名用户自行注册并创建自己的用户账户。

我们不打算允许自助注册,并且我们的要求规定用户账户由管理员创建。此外,我们已扩展 ApplicationUser 模型以包含一些附加属性。从功能角度来看,我们希望看到以下行为的实现

  • 查看用户账户列表(索引),并附带指向各种相关功能(编辑、角色等)的链接
  • 创建新用户(我们将为此借用“注册”方法,但我们会对其进行显著扩展)。
  • 编辑用户(管理员需要能够编辑用户账户、分配角色等)
  • 删除用户(我们希望能够删除用户账户(或至少使其变为活动或非活动))
  • 为用户分配角色
  • 登录
  • 注销

在继续之前,请理解我们可能会考虑上述无数可能的主要和次要变体。我选择了一个简单的应用程序模型,对于我们的目的来说,它相当随意。例如,您的应用程序要求可能允许匿名用户自行注册到某种默认角色。我这里表示的模型在范围上是相当有限的,因为我们试图在不被复杂实现分散注意力的情况下理解概念。我将把从这里扩展到更复杂(对您更有用)的应用程序模型留给您。

与上述一致,我已将 [Authorize(Roles = "Admin")] 属性添加到所有管理方法中,假设我们的管理角色将被称为(等等……)“Admin”。稍后会有更多相关内容。

我刚才说到哪儿了?

哦,是的。所以,看着上面列表中的功能需求,我将修改我的 AccountController 以合并上述项目。正如括号中提到的,我只是简单地借用 Register 控制器方法,并将其用于应该命名为 Create 的方法(此时出于懒惰!)。

修改 AccountController 注册方法(创建用户)

首先,我们将修改现有的 Register 方法,以适应我们新的 ApplicationUser 属性。我们希望能够在视图中创建一个新的 ApplicationUser,然后将新记录持久化到数据库中。

修改后的注册方法
[Authorize(Roles = "Admin")]
public ActionResult Register()
{
    return View();
}
  
  
[HttpPost]
[Authorize(Roles = "Admin")]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = model.GetUser();
        var result = await UserManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            return RedirectToAction("Index", "Account");
        }
    }
  
    // If we got this far, something failed, redisplay form
    return View(model);
}

在上面,我们没有做太多改动,只是使用了我们 RegisterViewModel 上定义的方便的 GetUser() 方法来检索 ApplicationUser 实例,该实例已填充并准备好持久化到我们的数据库中。此外,我们正在重定向到我们将在 AccountController 上临时定义的一个新的 Index 方法。

添加 AccountController Index 方法(查看用户列表)

以前,AccountController 没有 Index 方法。我们需要一种方式让我们的管理员查看应用程序的用户列表,并访问编辑、分配角色和删除功能。再次强调,访问用户账户数据是管理员功能,所以我们也添加了 [Authorize] 属性。

新的 Index 方法
[Authorize(Roles = "Admin")]
public ActionResult Index()
{
    var Db = new ApplicationDbContext();
    var users = Db.Users;
    var model = new List<EditUserViewModel>();
    foreach(var user in users)
    {
        var u = new EditUserViewModel(user);
        model.Add(u);
    }
    return View(model);
}

我们的 Index 方法目前使用 List<EditUserViewModel>,因为它包含了显示在列表中所需的所有信息。与我上面所说的相反,我在这里重用了 ViewModel。我可能应该修复它,但您可以自行决定这一点。

请注意,我没有在此方法中执行从 ApplicationUser 实例到每个 EditUserViewModel 的繁琐属性映射,而是简单地将 ApplicationUser 实例传递给 EditUserViewModel 上的重载构造函数。我们的 Index.cshtml 视图将期望 List<EditUserViewModel> 作为模型的显示。

添加 AccountController Edit 方法

我们添加了一个 Edit 方法,以方便管理员更新用户账户数据。在 Edit 方法实现中,至少在我的版本中,有一些细节需要注意。首先,虽然该方法仍然接受一个名为 id 的参数,但当请求路由到这里时,我们将实际传递给该方法的是一个 UserName。为什么?我决定遵循 ASP.NET 团队的领导。他们没有将用户 ID(GUID)传递到公共 HTML 中,所以我也不会。

此外,通过设计和约束,UserName 在我们的数据库中是唯一的,并且已经是一个半公开的信息。只需记住一点——公共账户 Action 方法的 id 参数将是用户(或,视情况而定,角色)名称,而不是整数。最后,我不想只为了重命名一个路由参数而添加一个全新的路由,该路由参数本质上与整数 id 具有相同的目的。

话虽如此,以下是新的 Edit 方法,当管理员希望更新用户信息时将使用它

新的编辑方法
[Authorize(Roles = "Admin")]
public ActionResult Edit(string id, ManageMessageId? Message = null)
{
    var Db = new ApplicationDbContext();
    var user = Db.Users.First(u => u.UserName == id);
    var model = new EditUserViewModel(user);
    ViewBag.MessageId = Message;
    return View(model);
}


[HttpPost]
[Authorize(Roles = "Admin")]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(EditUserViewModel model)
{
    if (ModelState.IsValid)
    {
        var Db = new ApplicationDbContext();
        var user = Db.Users.First(u => u.UserName == model.UserName);

        // Update the user data:
        user.FirstName = model.FirstName;
        user.LastName = model.LastName;
        user.Email = model.Email;
        Db.Entry(user).State = System.Data.Entity.EntityState.Modified;
        await Db.SaveChangesAsync();
        return RedirectToAction("Index");
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

在上述代码中,我们使用 Linq 根据在第一个 (GET) Edit 方法中作为 id 参数传入的 UserName 来获取特定用户的引用。然后,我们将 ApplicationUser 实例传递给构造函数,填充 EditUserViewModel 实例,并将 ViewModel 传递给 Edit.cshtml 视图。

当视图将我们更新后的模型返回到第二个(POST)Edit 方法时,我们以相反的方式做同样的事情。我们从数据库中检索用户记录,然后使用模型数据进行更新,并保存更改。

在我们的视图中,记住我们不能允许编辑 UserName 属性本身(至少在当前模型下,它将 UserName 视为不可侵犯的标识符)是很重要的。

添加 AccountController Delete 方法

我们正在向 AccountController 类添加一个 Delete (GET) 方法和一个 DeleteConfirmed (POST) 方法。在我的实现中,此方法将实际从数据库中删除选定的用户。您也可以选择将数据库记录标记为已删除,或实现其他方法来管理不需要的用户记录。

您也可以放弃删除方法,转而向 ApplicationUser 类添加一个布尔 Inactive 属性,并通过前面讨论的 Edit 方法管理活动/非活动状态。同样,这里的设计模型有许多排列。为了清晰起见,我选择了最简单的方法。

这里的 Delete 方法实现很简单,只要我们再次记住,传递给以下两个相关方法的 id 参数实际上是一个 UserName。

新的删除方法
[Authorize(Roles = "Admin")]
public ActionResult Delete(string id = null)
{
    var Db = new ApplicationDbContext();
    var user = Db.Users.First(u => u.UserName == id);
    var model = new EditUserViewModel(user);
    if (user == null)
    {
        return HttpNotFound();
    }
    return View(model);
}
  
  
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
[Authorize(Roles = "Admin")]
public ActionResult DeleteConfirmed(string id)
{
    var Db = new ApplicationDbContext();
    var user = Db.Users.First(u => u.UserName == id);
    Db.Users.Remove(user);
    Db.SaveChanges();
    return RedirectToAction("Index");
}

正如我们所看到的,DeleteConfirmed 方法带有 HttpPost 属性和 ActionName 属性“Delete”,这意味着路由到 Delete 的 POST 请求将路由到这里。这两个方法都使用作为 id 参数传递的 UserName 在数据库中查找相应的 ApplicationUser

再一次,与我之前所说的相反,我重用了 EditUserViewModel 来传递给 Delete.cshtml 视图。

向 AccountController 添加 UserRoles 方法

最后,我们添加 UserRoles 方法对。这是我们管理用户账户到各种应用程序角色的分配的地方。

这里的实现看起来相对简单,并且与我们目前检查过的其他控制器方法非常相似。然而,在 SelectUserRolesViewModelIdentityManager 类内部,有很多事情正在发生。首先是代码

新的 UserRoles 方法
[Authorize(Roles = "Admin")]
public ActionResult UserRoles(string id)
{
    var Db = new ApplicationDbContext();
    var user = Db.Users.First(u => u.UserName == id);
    var model = new SelectUserRolesViewModel(user);
    return View(model);
}
  
  
[HttpPost]
[Authorize(Roles = "Admin")]
[ValidateAntiForgeryToken]
public ActionResult UserRoles(SelectUserRolesViewModel model)
{
    if(ModelState.IsValid)
    {
        var idManager = new IdentityManager();
        var Db = new ApplicationDbContext();
        var user = Db.Users.First(u => u.UserName == model.UserName);
        idManager.ClearUserRoles(user.Id);
        foreach (var role in model.Roles)
        {
            if (role.Selected)
            {
                idManager.AddUserToRole(user.Id, role.RoleName);
            }
        }
        return RedirectToAction("index");
    }
    return View();
}

正如我们上面所看到的,路由到 UserRoles 方法的传入 GET 请求的处理方式与前面方法类似。作为 id 参数传递的 UserName 用于从数据库中检索用户记录,然后初始化 SelectUserRolesViewModel 实例,将 ApplicationUser 实例传递给构造函数。

有趣的事情来了。让我们再看看 AccountViewModels.cs 文件中 SelectUserRolesViewModel 的代码

SelectUserRolesViewModel 的代码 – 重新审视
public class SelectUserRolesViewModel
{
    public SelectUserRolesViewModel() 
    {
        this.Roles = new List<SelectRoleEditorViewModel>();
    }
  
  
    // Enable initialization with an instance of ApplicationUser:
    public SelectUserRolesViewModel(ApplicationUser user) : this()
    {
        this.UserName = user.UserName;
        this.FirstName = user.FirstName;
        this.LastName = user.LastName;
  
        var Db = new ApplicationDbContext();
  
        // Add all available roles to the list of EditorViewModels:
        var allRoles = Db.Roles;
        foreach(var role in allRoles)
        {
            // An EditorViewModel will be used by Editor Template:
            var rvm = new SelectRoleEditorViewModel(role);
            this.Roles.Add(rvm);
        }
  
        // Set the Selected property to true for those roles for 
        // which the current user is a member:
        foreach(var userRole in user.Roles)
        {
            var checkUserRole = 
                this.Roles.Find(r => r.RoleName == userRole.Role.Name);
            checkUserRole.Selected = true;
        }
    }
  
    public string UserName { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public List<SelectRoleEditorViewModel> Roles { get; set; }
}

在初始化期间,我们正在使用应用程序中所有可用的角色填充 List<SelectRoleEditorViewModel>()。首先,这是一个重构的主要候选者,因为我正在对象构造函数中执行数据访问(通常是不允许的)。其次,SelectRoleEditorViewModel?什么?

在我当前的实现中,我们将看到 SelectUserRolesViewModel 被传递给 UserRoles.cshtml 视图。我们希望显示基本的用户详细信息(这样我们就可以知道正在为哪个用户分配角色——始终很重要),以及所有可用角色的列表。我决定使用复选框来促进角色分配,通过选中一个或多个(或不选中!)复选框来将角色分配给用户。

这就是 EditorViewModel 的用武之地。我们将使用一种向表格添加复选框并允许用户从项目列表中选择的常用技术。

让我们重新审视一下在 AccountViewModels.cs 文件中定义的 SelectRoleEditorViewModel 的代码。

SelectRoleEditorViewModel 表示一个单独的角色,从以下代码中我们可以看到,它包含一个布尔字段,用于指示该角色的 Selected 状态

SelectRoleEditorViewModel 的代码,重新审视
public class SelectRoleEditorViewModel
{
    public SelectRoleEditorViewModel() { }
    public SelectRoleEditorViewModel(IdentityRole role)
    {
        this.RoleName = role.Name;
    }
  
    public bool Selected { get; set; }
  
    [Required]
    public string RoleName { get; set; }
}

这个 EditorViewModel 将被一个称为*EditorTemplate*的特殊视图使用,我们很快就会看到。目前,请记住 SelectUserRolesViewModel 包含一个 SelectRoleEditorViewModel 对象的列表(是的,这些对象的命名可以更好,并且带来了一些挑战——我在这里乐于接受建议!目前,我尽量将它们视为“SelectUserRoles-ViewModel”和“SelectRole-EditorViewModel”,如果这有帮助的话)。

这涵盖了我们 AccountController 中修改或新增的项目。现在让我们看看我们的视图。

基于角色的身份管理的基本视图

我们已经有了一些所需的视图,只是需要稍微修改一下。此外,我们还需要添加一些新的视图。我们将从修改现有视图以适应我们的需求开始,从 Register.cshmtl 视图开始。

修改 Register.cshtml 视图文件

Register 视图目前的设计是为了允许用户自行注册。由于我们已经将 AccountController 上的 Register 方法用于受限制的管理用途,所以我们也将 Register.cshtml 视图用于我们的目的。

本质上,我们只需要向文件添加一些额外的 HTML 和 Razor 语法代码,以适应我们添加到 ApplicationUser 类的新属性

修改 Register.cshtml 文件
@model AspNetRoleBasedSecurity.Models.RegisterViewModel
@{
    ViewBag.Title = "Register";
}
  
<h2>@ViewBag.Title.</h2>
  
@using (Html.BeginForm("Register", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
    <h4>Create a new account.</h4>
    <hr />
    @Html.ValidationSummary()
    <div class="form-group">
        @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.UserName, 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>
  
    // Add the LastName, FirstName, and Email Properties:
    <div class="form-group">
        @Html.LabelFor(m => m.LastName, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.LastName, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.FirstName, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.FirstName, new { @class = "form-control" })
        </div>
    </div>
    <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">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" class="btn btn-default" value="Register" />
        </div>
    </div>
}

接下来,我们将创建编辑视图。为此,我们可以右键单击 AccountController 中的 Edit Action 方法声明,让 VS 为我们完成工作

为 Edit 方法创建视图

add-view-edit-method

接下来我们看到“添加视图”对话框。从“模板”下拉列表中选择“编辑”模板,并从“模型类”下拉列表中选择 EditUserViewModel 类。我们已经有一个数据上下文,所以将其留空。

添加视图对话框

add-view-dialog

DeleteIndex 方法重复上述过程。为每个方法选择适当的模板(对于 Index 视图使用列表模板,因为我们要显示用户账户列表),并使用 EditUserViewModel 作为两者的模型类。

微调索引视图

我们需要对索引视图进行一些小的更改。

请注意底部附近,模板提供了用于编辑、详细信息和删除的便捷链接。我们将更改“详细信息”链接,使其指向我们的 UserRoles Action 方法。此外,我们需要替换被注释掉的路由参数,以便将 Username 作为 id 参数传递。

最后,文件顶部附近有一些用于创建新用户的 Action 链接的 Razor 代码。将 Action 方法参数“Create”替换为我们借用的“Register”方法。

修改后,最终的 Index.cshtml 文件应如下所示

修改后的 Index.cshtml 文件
@model IEnumerable<AspNetRoleBasedSecurity.Models.EditUserViewModel>
  
@{
    ViewBag.Title = "Index";
}
  
<h2>Index</h2>
  
<p>
    @Html.ActionLink("Create New", "Register") 
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.UserName)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.FirstName)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.LastName)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Email)
        </th>
        <th></th>
    </tr>
  
@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.UserName)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.FirstName)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.LastName)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Email)
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id = item.UserName }) |
            @Html.ActionLink("Roles", "UserRoles", new { id = item.UserName }) |
            @Html.ActionLink("Delete", "Delete", new { id = item.UserName })
        </td>
    </tr>
}
  
</table>

现在我们可以回到用户角色问题了。

创建 UserRoles.cshtml 视图

我们可以使用 VS Add View 方法创建我们的 UserRoles.cshtml 视图,就像我们创建前面的视图一样。但是,我们将在此视图上从头开始进行大部分工作。右键单击 AccountControllerUserRoles 方法并选择添加视图。但是,这次,从模板下拉列表中选择空模板选项,并从模型类下拉列表中选择 SelectUserRolesViewModel

您现在应该有一个几乎为空的 UserRoles.cshtml 文件,看起来像这样

空的 UserRoles.cshtml 文件
@model AspNetRoleBasedSecurity.Models.SelectUserRolesViewModel
  
@{
    ViewBag.Title = "UserRoles";
}
  
<h2>UserRoles</h2>

从这里开始,我们将手动添加代码。我们想要显示基本的用户信息,然后是用户可以被分配(或移除)的所有可用角色的列表。我们希望角色列表以复选框作为选择机制。

为了实现上述目标,我们将按如下方式添加 Razor 代码

添加到 UserRoles.cshtml 文件中的代码
@model AspNetRoleBasedSecurity.Models.SelectUserRolesViewModel
  
@{
    ViewBag.Title = "User Roles";
}
  
<h2>Roles for user @Html.DisplayFor(model => model.UserName)</h2>
<hr />
  
@using (Html.BeginForm("UserRoles", "Account", FormMethod.Post, new { encType = "multipart/form-data", name = "myform" }))
{
    @Html.AntiForgeryToken()
  
    <div class="form-horizontal">
        @Html.ValidationSummary(true)
        <div class="form-group">
            <div class="col-md-10">
                @Html.HiddenFor(model => model.UserName)
            </div>
        </div>
  
        <h4>Select Role Assignments</h4>
        <br />
        <hr />
  
        <table>
            <tr>
                <th>
                    Select
                </th>
                <th>
                    Role
                </th>
            </tr>
        @Html.EditorFor(model => model.Roles)
        </table>
        <br />
        <hr />
  
        <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>
}

在上述代码中,我们设置了一个以 UserName 为特色的显示标题,并创建了一个 HTML 表格,其中包含 SelectRole 的标题元素。Select 列将包含每个角色的复选框,而 Role 列显然将显示角色名称。在表格标题设置下方,请注意这一行

@Html.EditorFor(model => model.Roles)

这就是我们回到 *EditorTemplate* 概念的地方。编辑器模板是一个共享视图,需要位于项目中的 *Views => Shared => EditorTemplates* 文件夹中。您可能需要自己创建该文件夹。通过右键单击新的 *EditorTemplates* 文件夹并选择添加视图来创建 SelectRoleEditorViewModel 编辑器模板。再次使用空模板,并将视图命名为 SelectRoleEditorViewModel(这很重要)。从模型类下拉列表中选择 SelectRoleEditorViewModel。完成后,您应该会得到一个 .cshtml 文件,如下所示

空的 SelectRoleEditorViewModel 编辑器模板文件
@model AspNetRoleBasedSecurity.Models.SelectRoleEditorViewModel
  
@{
    ViewBag.Title = "SelectRoleEditorViewModel";
}
  
<h2>SelectRoleEditorViewModel</h2>

从这里,我们将添加几行,使我们的文件看起来像这样

修改后的 SelectRoleEditorViewModel 编辑器模板文件
@model AspNetRoleBasedSecurity.Models.SelectRoleEditorViewModel
@Html.HiddenFor(model => model.RoleName)
<tr>
    <td style="text-align:center">
        @Html.CheckBoxFor(model => model.Selected)
    </td>
    <td>
        @Html.DisplayFor(model => model.RoleName)
    </td>
</tr>

现在我们有了一个 SelectRoleEditorViewModel 的编辑器模板。我们 UserRoles.cshtml 视图中的代码将使用此模板来呈现我们的角色列表,包括复选框。在提交表单时,复选框中的选择将反映在我们的模型中,并与角色名称一起返回给控制器。

在主站点页面添加管理员选项卡

我们快完成了。然而,如果我们不能从应用程序内部访问这些新的视图和功能,它们就没什么用处。我们需要在主站点页面上添加一个管理员选项卡,并取消匿名用户访问网站默认包含的注册链接的能力。为此,我们需要修改Views => Shared文件夹中的 _Layout.cshtml 文件。

修改后的 _Layout.cshtml 文件
<div class="navbar-collapse collapse">
    <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>
        <li>@Html.ActionLink("Admin", "Index", "Account")</li>
    </ul>
    @Html.Partial("_LoginPartial")
</div>

上面的代码来自 _Layout.cshtml 视图文件的中间位置。添加“Admin”ActionLink 以创建一个指向我们 AccountController 的 Index 方法的选项卡链接。

从 _LoginPartial.cshtml 视图中删除注册链接

最后,我们希望从主站点布局中删除指向 Register 方法的链接。此链接位于 _LoginPartial.cshtml 文件中,同样位于 Views => Shared 文件夹中。在该文件的底部,删除“Register”Action Link

从 _LoginPartial.cshtml 中删除注册链接
else
{
    <ul class="nav navbar-nav navbar-right">
        <li>@Html.ActionLink("Register", "Register", "Account", routeValues: null, htmlAttributes: new { id = "registerLink" })</li>
        <li>@Html.ActionLink("Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })</li>
    </ul>
}

现在我们所有的视图都应该准备就绪了。

设置并运行 Entity Framework 迁移

现在我们大部分部件都已到位,是时候使用 Entity Framework 设置 Code-First Migrations 了。此外,由于我们表面上正在构建一个只有管理员角色用户才能创建或编辑用户的应用程序,我们需要为应用程序播种一个初始管理员用户。此外,由于我们不打算允许创建或修改角色,我们需要使用我们期望在应用程序中使用的角色来播种数据库,因为我们无法在应用程序内部创建它们。我们在上一篇文章中相当彻底地介绍了 EF Code-First Migrations,所以这次我将略过它。首先,在包管理器控制台中输入以下内容启用迁移

在项目中启用 EF Migrations
PM> Enable-Migrations –EnableAutomaticMigrations

现在,打开 Migrations => Configuration.cs 文件并添加以下代码(根据您的具体情况进行调整。您可能不想将我的信息作为您的管理员用户。另请注意,您提供的任何密码都必须符合应用程序的约束,这似乎要求至少包含一个大写字母和一个数字)

使用种子数据修改 EF 迁移配置文件
using AspNetRoleBasedSecurity.Models;
using System.Data.Entity.Migrations;

namespace AspNetRoleBasedSecurity.Migrations
{
    using System;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;
  
    internal sealed class Configuration : DbMigrationsConfiguration<ApplicationDbContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
        }
  
  
        protected override void Seed(ApplicationDbContext context)
        {
            this.AddUserAndRoles();
        }
  
  
        bool AddUserAndRoles()
        {
            bool success = false;
  
            var idManager = new IdentityManager();
            success = idManager.CreateRole("Admin");
            if (!success == true) return success;
  
            success = idManager.CreateRole("CanEdit");
            if (!success == true) return success;
  
            success = idManager.CreateRole("User");
            if (!success) return success;
  
  
            var newUser = new ApplicationUser()
            {
                UserName = "jatten",
                FirstName = "John",
                LastName = "Atten",
                Email = "jatten@typecastexception.com"
            };
  
            // Be careful here - you  will need to use a password which will 
            // be valid under the password rules for the application, 
            // or the process will abort:
            success = idManager.CreateUser(newUser, "Password1");
            if (!success) return success;
  
            success = idManager.AddUserToRole(newUser.Id, "Admin");
            if (!success) return success;
  
            success = idManager.AddUserToRole(newUser.Id, "CanEdit");
            if (!success) return success;
  
            success = idManager.AddUserToRole(newUser.Id, "User");
            if (!success) return success;
  
            return success;
        }
    }
}

完成后,从包管理器控制台运行以下命令

添加初始 EF 迁移
Add-Migration Init

然后通过运行 Update-Database 命令创建数据库

更新数据库命令
Update-Database

如果一切顺利,您的数据库应作为 SQL Server(本地)数据库在 App_Data 文件夹中创建。如果您想指向不同的数据库服务器,请查看上一篇文章,其中我们讨论了将默认连接字符串指向不同的服务器。您可以通过在 Visual Studio 中打开服务器资源管理器窗口来检查数据库是否已正确创建。或者,当然,您也可以简单地运行您的应用程序,看看会发生什么!

使用 [Authorize] 属性控制访问

从这一点开始,我们可以使用 [Authorize] 属性来管理对不同应用程序功能的访问。我们已经在 AccountController 中的方法上看到了这方面的示例,其中除 Login 方法之外的所有访问都仅限于 Admin 角色的用户。

使用 [Authorize] 属性控制对功能的访问
[AllowAnonymous]
public MyPublicMethod()
{
    // Code
}
  
  
[Authorize(Role = "Admin, CanEdit, User")]
public MyPrettyAccessibleMethod()
{
    // Code
}
  
  
[Authorize(Role = "Admin, CanEdit")]
public MyMoreRestrictiveMethod()
{
    // Code
}
  
  
[Authorize(Role = "Admin")]
public MyVeryRestrictedMethod()
{
    // Code
}

在上面的代码中,我们看到基于使用 [Authorize] 属性定义的角色访问,方法访问逐渐受到更多限制。目前,我们的角色定义并非以更高级别角色继承受限角色相关权限的方式“分层”。例如,如果一个方法被 [Authorize] 属性修饰,授予用户角色成员访问权限,需要注意的是,只有该角色的成员才能访问该方法。在这种情况下,角色访问必须为每个角色明确和具体地授予。

角色权限不可继承
// Admins can't access this method:
[Authorize(Role = "Users")]
public SomeMethod()
{
    // Code
}

// Admins AND Users can access this method:
[Authorize(Role = "Users, Admins")]
public SomeMethod()
{
    // Code
}

与我们使用大多数操作系统安全性的经验相反,Admin 角色的成员不会自动获得 User 角色的所有权限。我们可能可以实现这一点,但这超出了本文的范围。

关于角色和角色命名的一些说明

出于本文的目的,我使用了一些相当通用的角色名称,因为在使用角色管理应用程序访问时,我们实际上没有需要考虑的业务案例。ASP.NET 团队建议(我同意)最好使用描述性和限制性的角色定义,这些定义在一定程度上描述了与该角色相关的权限。例如,与其使用通用的“Admin”角色,不如创建一个专门用于账户和身份管理的“IdentityManager”角色,以及其他在您的应用程序业务上下文中具有意义的描述性角色名称。

总结

在本文中,我们创建了一个非常简单的基于角色的身份管理实现。正如我在开头提到的,这里使用的模型,脱离任何业务上下文,有些基础。我试图展示使用 ASP.NET MVC 身份系统的一些基础知识,扩展它以包含一些自定义属性,并修改其用途以适应基本的业务用例。关于Web 应用程序安全性有很多知识,在我看来,这里不适合重新发明轮子。在本文讨论的应用程序中,我们重新调整了 ASP.NET 身份模型的组件,但我们使用了设计好的核心部分,而不是发明我们自己的授权机制。希望这篇相当长的文章有所帮助,并且我没有传播任何错误信息。如果您发现任何重大问题,请务必通过评论或电子邮件告诉我。我将及时纠正它们。

其他资源和感兴趣的项目

身份验证 v2.0
身份验证 1.0 及其他项目

扩展 ASP.NET MVC 5 中的身份账户并实现基于角色的认证 - CodeProject - 代码之家
© . All rights reserved.