ASP.NET Identity 2.0:自定义用户和角色
ASP.NET Identity 团队已于三月发布了 Identity 2.0 框架 RTM。新版本在原始 1.0 版本的功能基础上进行了重大添加,并引入了一些重大更改。在之前的文章中,我们从高层角度探讨了 Identity 2.0 的工作原理。
ASP.NET Identity 团队已于三月发布了 Identity 2.0 框架 RTM。新版本在原始 1.0 版本的功能基础上进行了重大添加,并引入了一些重大更改。
在之前的文章中,我们通过深入研究 Identity Samples 应用程序(该应用程序过去和现在都处于测试阶段,因此可能会继续发生变化)从高层角度了解了 Identity 2.0 的工作原理。我们还详细探讨了如何实现电子邮件账户确认和双因素身份验证,这代表了 Identity 2.0 RTM 版本中一些更吸引人的功能。
- 核心 Identity 2.0 类型是泛型的
- 扩展 Identity User - 这是最简单的部分
- 扩展 Identity Role
- 关于 IdentityRole 和 IdentityUserRole 的注意事项
- 重新实现 RoleStore 和 ApplicationRoleManager
- 其他资源和感兴趣的项目
- 在 Github 上克隆示例项目
我们之前已经探讨了 Identity 1.0 下的扩展 Identity 账户和实现基于角色的身份验证,以及扩展和修改角色。然而,自那时以来,情况已经发生了变化。如果您正在使用 Identity 1.0,那些文章仍然适用,您现在应该参考它们。如果您正在深入研究 Identity 2.0,请继续阅读!
我们之前在 Identity 1.0 版本下需要自行添加的许多自定义功能,现在已经整合到 2.0 RTM 版本中。具体来说,角色管理以及将用户分配到一个或多个角色的功能在 Identity Samples 项目中已开箱即用。扩展基本的 IdentityUser 和 Role 类是一个更灵活的方案,但比以前更复杂。
在这篇文章中,我们将深入研究,看看我们需要做些什么,通过为每个基本 ApplicationUser
和 ApplicationRole
类型添加一些自定义属性来扩展它们。
Identity 2.0 和 Web Api
如果您希望在 Web Api 上下文中对 Identity 2.0 进行类似的自定义,情况会有些不同。您可能会对以下文章感兴趣
- ASP.NET Identity 2.0:Identity 2.0 和 Web API 2.2 入门
- ASP.NET Web API 和 Identity 2.0 - 自定义 Identity 模型和实现基于角色的授权
更新:如果您希望使用整数键而不是字符串,请参阅 ASP.NET Identity 2.0 扩展身份模型并使用整数键而非字符串。
我还创建了一个开箱即用的 易于扩展的 Identity 2.0 项目模板,它比本文中的示例更进一步。
克隆本文的源代码
我们将在这里逐步讲解,以便您可以跟着操作。不过,我已经在 GitHub 上创建了一个包含已完成项目源代码的仓库。如果您遇到问题,我强烈建议克隆源代码以仔细查看。
请记住,这里的代码是极简的,我们不尝试进行任何 UI 改进、过多的验证或其他从应用程序设计角度来看可能显而易见的事情。相反,我们尽量保持简单,以便我们可以专注于当前的主题。
我们稍后会讨论所有这些。首先,我们将从安装 Identity Samples 项目开始。
安装 Identity 2.0 示例项目
Identity 团队创建了一个示例项目,可以将其安装到空的 ASP.NET Web 项目中。请注意,截至本文撰写之时,这是一个 Alpha 版本,因此某些内容可能会发生变化。然而,大多数基本功能都已实现,事实上,该示例项目是您在自己的网站中使用 Identity 2.0 的一个强大起点。
Identity Samples 项目可在 Nuget 上获得。首先,创建一个空的 ASP.NET Web 项目(务必使用“空”模板,而不是 MVC,也不是 Webforms,是“空”)。然后打开包管理器控制台并输入
PM> Install-Package Microsoft.AspNet.Identity.Samples -Pre
这可能需要一两分钟才能运行。完成后,您将在 VS 解决方案资源管理器中看到一个基本的 ASP.NET MVC 项目。仔细查看 Identity 2.0 示例项目,熟悉各项内容及其位置。
使用自定义类型重新设计 Identity Samples 项目
Identity Samples 项目提供了一个坚实的平台,可以作为将 Identity 2.0 框架集成到新的 ASP.NET MVC 项目中的基础。然而,该项目本身假定您将使用默认的字符串键(这在我们的数据库中转换为基于字符串的主键),并且还假定您将使用 Identity Samples 开箱即用的默认类型。
正如我们将看到的,Identity 2.0 为实现派生自构成 Identity 框架核心的接口和泛型基类的自定义类型提供了极大的灵活性。然而,从头开始构建一个类似于 Identity Samples 的项目将是一项巨大的工作。我们将利用 Identity 团队在创建示例项目方面所做的工作,而不是从头开始,我们将调整这个优秀的平台来实现我们自己的自定义。
核心 Identity 2.0 对象是泛型的
Identity 团队在 Identity Samples 项目中实现的基本类型代表了一个抽象层,位于一组更灵活的基类之上,这些基类使用泛型类型参数。例如,如果我们查看 IdentityModels.cs 代码文件,我们可以看到 ApplicationUser
是从 IdentityUser
派生而来的
Identity Samples 项目中实现的应用程序用户
class ApplicationUser : IdentityUser
{
async Task<ClaimsIdentity> GenerateUserIdentityAsync(
UserManager<ApplicationUser> manager)
{
var userIdentity = await manager.CreateIdentityAsync(
this, DefaultAuthenticationTypes.ApplicationCookie);
return userIdentity;
}
}
IdentityUser
在此情况下属于命名空间 Microsoft.AspNet.Identity.EntityFramework
。我们可以使用 VS 的“转到定义”功能或反编译器(例如 Telerik 的 Just Decompile 或 Redgate 的 Reflector)来仔细查看 IdentityUser
Identity 用户类
class IdentityUser :
IdentityUser<string, IdentityUserLogin, IdentityUserRole,
IdentityUserClaim>, IUser, IUser<string>
{
IdentityUser()
{
this.Id = Guid.NewGuid().ToString();
}
IdentityUser(string userName) : this()
{
this.UserName = userName;
}
}
在这里,我们看到 IdentityUser
继承自另一个基类 IdentityUser<TKey, TLogin, TRole, TClaim>
以及几个接口。在这种情况下,具体的 IdentityUser 将特定的类型参数传递给泛型基类。这就是事情开始变得有趣的地方。
事实证明,所有使用 Identity 2.0 所需的基本类型都以泛型基类型开始,具有类似的类型参数,允许我们定义自定义实现。查看用于构建 Identity Samples 项目的核心 Identity 组件的定义,我们找到了以下类,此处以默认 Identity 构造中使用的具体类型参数表示
带有默认类型参数的默认 Identity 2.0 类签名
class IdentityUserRole
: IdentityUserRole<string>
class IdentityRole
: IdentityRole<string, IdentityUserRole>
class IdentityUserClaim
: IdentityUserClaim<string>
class IdentityUserLogin
: IdentityUserLogin<string>
class IdentityUser
: IdentityUser<string, IdentityUserLogin,
IdentityUserRole, IdentityUserClaim>, IUser, IUser<string>
class IdentityDbContext
: IdentityDbContext<IdentityUser, IdentityRole, string,
IdentityUserLogin, IdentityUserRole, IdentityUserClaim>
class UserStore<TUser>
: UserStore<TUser, IdentityRole, string, IdentityUserLogin,
IdentityUserRole, IdentityUserClaim>,
IUserStore<TUser>, IUserStore<TUser, string>, IDisposable
where TUser : IdentityUser
class RoleStore<TRole>
: RoleStore<TRole, string, IdentityUserRole>, IQueryableRoleStore<TRole>,
IQueryableRoleStore<TRole, string>, IRoleStore<TRole, string>, IDisposable
where TRole : IdentityRole, new()
在上面,我们可以看到类型之间存在一种相互依赖的进展。IdentityUserRole
派生自 IdentityUserRole<TKey>
,其中字符串作为单个必需的类型参数。IdentityRole
派生自 IdentityRole<TKey, TIdentityUserRole>
,分别使用字符串的具体类型和 IdentityUserRole
的默认实现(正如我们所见,它指定 string
作为键类型),等等。当我们向下移动列表时,具体类型实现之间的相互依赖性增加。
我们稍后将看到这如何影响我们自定义用户和角色类型的能力。首先,我们可以看到向 Identity Samples 项目提供的 ApplicationUser
实现添加简单属性是多么容易。
扩展 Identity User - 最简单的部分
如果我们要做的只是向 Identity Samples 项目中定义的默认 ApplicationUser
类添加一些额外的属性,那么生活就足够简单——Identity Samples 团队已经为项目设置了一个合理的默认实现,只需很少的努力就可以对其进行扩展。
回想一下前面提到的 ApplicationUser
类。假设我们要添加一些地址属性,如下所示
扩展默认的 ApplicationUser 类
class ApplicationUser : IdentityUser
{
async Task<ClaimsIdentity>
GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
var userIdentity = await manager
.CreateIdentityAsync(this,
DefaultAuthenticationTypes.ApplicationCookie);
return userIdentity;
}
string Address { get; set; }
string City { get; set; }
string State { get; set; }
// Use a sensible display name for views:
[Display(Name = "Postal Code")]
string PostalCode { get; set; }
// Concatenate the address info for display in tables and such:
string DisplayAddress
{
get
{
string dspAddress =
string.IsNullOrWhiteSpace(this.Address) ? "" : this.Address;
string dspCity =
string.IsNullOrWhiteSpace(this.City) ? "" : this.City;
string dspState =
string.IsNullOrWhiteSpace(this.State) ? "" : this.State;
string dspPostalCode =
string.IsNullOrWhiteSpace(this.PostalCode) ? "" : this.PostalCode;
return string
.Format("{0} {1} {2} {3}", dspAddress, dspCity, dspState, dspPostalCode);
}
}
}
从这里开始,在这种有限的情况下,我们所需要做的就是更新各种 ViewModel、View 和 Controller,以包含我们的新属性。我们将为我们的 RegisterViewModel
、Register.cshtml View 本身以及 AccountsController
的 Register
方法添加新属性的功能。
我们还将对 UsersAdminController
以及相关的 ViewModel 和 View 执行相同的操作。
更新注册 ViewModel 以包含地址信息
RegisterViewModel
在 AccountViewModels.cs 文件中定义。我们需要将新属性添加到此 ViewModel 中,以便新用户注册的 Register 视图能够提供输入地址信息的机会
更新 RegisterViewModel 以包含地址信息
class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email")]
string Email { get; set; }
[Required]
[StringLength(100, ErrorMessage =
"The {0} must be at least {2} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage =
"The password and confirmation password do not match.")]
string ConfirmPassword { get; set; }
// Add the new address properties:
string Address { get; set; }
string City { get; set; }
string State { get; set; }
// Use a sensible display name for views:
[Display(Name = "Postal Code")]
string PostalCode { get; set; }
}
更新注册视图以包含地址信息
显然,我们希望用户在注册时能够输入地址信息。Register.cshtml 视图位于解决方案资源管理器中的 Views => Accounts 文件夹中。按如下方式更新
用地址信息更新注册视图
@model IdentitySample.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("", new { @class = "text-danger" })
<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.Address, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.Address, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.City, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.City, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.State, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.State, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.PostalCode, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.PostalCode, 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">
<div class="col-md-offset-2 col-md-10">
<input type="submit" class="btn btn-default" value="Register" />
</div>
</div>
}
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
我们可以在上面黄色高亮区域看到我们已将相应的字段添加到视图模板中。
更新 AccountController 上的注册方法
现在我们需要确保在提交表单数据时保存地址信息。更新 AccountController
上的 Register()
方法
更新 AccountController 上的注册方法
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser
{
UserName = model.Email,
Email = model.Email
};
// Add the Address properties:
user.Address = model.Address;
user.City = model.City;
user.State = model.State;
user.PostalCode = model.PostalCode;
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
var callbackUrl = Url.Action("ConfirmEmail", "Account",
new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
await UserManager.SendEmailAsync(user.Id,
"Confirm your account",
"Please confirm your account by clicking this link: <a href=\""
+ callbackUrl + "\">link</a>");
ViewBag.Link = callbackUrl;
return View("DisplayEmail");
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
更新用户管理组件以使用新属性
现在,基本的注册功能已更新,以利用新的地址属性。然而,Identity Samples 项目还提供了一些管理功能,通过这些功能,管理员角色成员可以查看和编辑用户信息。
我们还需要在此处更新一些 ViewModel、View 和 Controller 方法。
更新 UsersAdmin/Create.cshtml 用户视图
在 Views => UsersAdmin 文件夹中,Create.cshtml 视图使用熟悉的 RegisterViewModel
允许系统管理员向系统中添加新用户。我们希望在此处也提供地址信息的数据输入
更新 UsersAdmin/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.Address, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.Address, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.City, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.City, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.State, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.State, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.PostalCode, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.PostalCode, 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 Role
</label>
<div class="col-md-10">
@foreach (var item in (SelectList)ViewBag.RoleId)
{
<input type="checkbox" name="SelectedRoles"
value="@item.Value" class="checkbox-inline" />
@Html.Label(item.Value, new { @class = "control-label" })
}
</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")
}
同样,我们可以通过上面高亮区域看到我们需要更新视图的地方。
更新编辑用户 ViewModel
EditUserViewModel
被 UserAdminController
和相关视图用来支持编辑用户信息。我们需要在此处包含我们希望能够编辑的任何新属性。EditUserViewModel
在 AdminViewModels.cs 代码文件中定义。按如下方式更新
更新 EditUserViewModel
public class EditUserViewModel
{
public string Id { get; set; }
[Required(AllowEmptyStrings = false)]
[Display(Name = "Email")]
[EmailAddress]
public string Email { get; set; }
// Add the Address Info:
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
// Use a sensible display name for views:
[Display(Name = "Postal Code")]
public string PostalCode { get; set; }
public IEnumerable<SelectListItem> RolesList { get; set; }
}
更新 EditUser.cshtml 视图
现在我们的 EditUserViewModel
已经更新,我们再次需要将相应的字段添加到 EditUser.cshtml 视图中,该视图也位于 Views => UsersAdmin 文件夹中
更新 EditUser.cshtml 视图
@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.LabelFor(model => model.Address, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.Address, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.City, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.City, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.State, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.State, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.PostalCode, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.PostalCode, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.Label("Roles", new { @class = "control-label col-md-2" })
<span class=" col-md-10">
@foreach (var item in Model.RolesList)
{
<input type="checkbox" name="SelectedRole" value="@item.Value" checked="@item.Selected" class="checkbox-inline" />
@Html.Label(item.Value, new { @class = "control-label" })
}
</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")
}
更新 Users Admin Index、Delete 和 Detail 视图
对于 UserAdmin 的 Index、Delete 和 Detail 视图,我们将采取略有不同的做法。我们将使用 DisplayAddress 属性将地址信息连接成一行,适用于在表格或单个表单标签中显示。为了简洁起见,我们仅在此处更新 Index 视图,该视图也位于 Views => UsersAdmin 中
更新 UsersAdmin Index.cshtml 视图
@model IEnumerable<IdentitySample.Models.ApplicationUser>
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
<table class="table">
<tr>
<th>
@Html.DisplayNameFor(model => model.UserName)
</th>
@*Add a table header for the Address info:*@
<th>
@Html.DisplayNameFor(model => model.DisplayAddress)
</th>
<th>
</th>
</tr>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.UserName)
</td>
<td>
@*Add table data for the Address info:*@
@Html.DisplayFor(modelItem => item.DisplayAddress)
</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>
在上面我们可以看到,我们所做的只是添加了一个表头元素和一个表数据元素来显示 DisplayAddress
数据(它是一个伪装成属性的函数)。同样的操作也可以用于 Delete.cshtml 视图和 Details.cshtml 视图,所以我们在这里就不重复了。
更新用户管理控制器
现在我们已经更新了相关的 ViewModel 和 View,我们还需要更新 UserAdminController
上相应的控制器动作,以便模型数据能够正确地在 View 之间传递。具体来说,我们需要修改 Create()
和 Edit()
方法。
更新 UserAdminController 上的 Create 和 Edit 方法
create 方法允许管理员创建一个新的系统用户。添加功能以在新用户创建时包含新的地址属性
UserAdminController 上修改后的 Create 方法
[HttpPost]
public async Task<ActionResult> Create(RegisterViewModel userViewModel, params string[] selectedRoles)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser
{
UserName = userViewModel.Email, Email =
userViewModel.Email,
// Add the Address Info:
Address = userViewModel.Address,
City = userViewModel.City,
State = userViewModel.State,
PostalCode = userViewModel.PostalCode
};
// Add the Address Info:
user.Address = userViewModel.Address;
user.City = userViewModel.City;
user.State = userViewModel.State;
user.PostalCode = userViewModel.PostalCode;
// Then create:
var adminresult = await UserManager.CreateAsync(user, userViewModel.Password);
//Add User to the selected Roles
if (adminresult.Succeeded)
{
if (selectedRoles != null)
{
var result = await UserManager.AddToRolesAsync(user.Id, selectedRoles);
if (!result.Succeeded)
{
ModelState.AddModelError("", result.Errors.First());
ViewBag.RoleId = new SelectList(await RoleManager.Roles.ToListAsync(), "Name", "Name");
return View();
}
}
}
else
{
ModelState.AddModelError("", adminresult.Errors.First());
ViewBag.RoleId = new SelectList(RoleManager.Roles, "Name", "Name");
return View();
}
return RedirectToAction("Index");
}
ViewBag.RoleId = new SelectList(RoleManager.Roles, "Name", "Name");
return View();
}
接下来,以类似的方式更新 Edit()
方法。首先,我们需要在从 [Get] 方法将其传递给 View 之前,使用地址信息填充 EditUserViewModel
UserAdmin 控制器 Edit 方法的修改后的 [Get] 实现
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();
}
var userRoles = await UserManager.GetRolesAsync(user.Id);
return View(new EditUserViewModel()
{
Id = user.Id,
Email = user.Email,
// Include the Addresss info:
Address = user.Address,
City = user.City,
State = user.State,
PostalCode = user.PostalCode,
RolesList = RoleManager.Roles.ToList().Select(x => new SelectListItem()
{
Selected = userRoles.Contains(x.Name),
Text = x.Name,
Value = x.Name
})
});
}
然后,我们需要更新 [Post] 重写。但是,请仔细注意,还要在参数中包含额外的绑定
UserAdminController 上修改后的 Edit 方法 [Post]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit([Bind(Include =
"Email,Id,Address,City,State,PostalCode")]
EditUserViewModel editUser, params string[] selectedRole)
{
if (ModelState.IsValid)
{
var user = await UserManager.FindByIdAsync(editUser.Id);
if (user == null)
{
return HttpNotFound();
}
user.UserName = editUser.Email;
user.Email = editUser.Email;
user.Address = editUser.Address;
user.City = editUser.City;
user.State = editUser.State;
user.PostalCode = editUser.PostalCode;
var userRoles = await UserManager.GetRolesAsync(user.Id);
selectedRole = selectedRole ?? new string[] { };
var result = await UserManager.AddToRolesAsync(user.Id,
selectedRole.Except(userRoles).ToArray<string>());
if (!result.Succeeded)
{
ModelState.AddModelError("", result.Errors.First());
return View();
}
result = await UserManager.RemoveFromRolesAsync(user.Id,
userRoles.Except(selectedRole).ToArray<string>());
if (!result.Succeeded)
{
ModelState.AddModelError("", result.Errors.First());
return View();
}
return RedirectToAction("Index");
}
ModelState.AddModelError("", "Something failed.");
return View();
}
基于角色的授权已实现
当我们查看文章扩展 Identity 用户和实现基于角色的授权中对 Identity 1.0 的自定义时,我们需要显著修改基本项目才能将用户分配到角色。默认的 ASP.NET MVC 项目中没有直接管理用户角色分配的规定。
Identity Samples 项目解决了这一缺陷,并开箱即用地实现了用户/角色管理。我们以前需要自己编写控制器方法、模型和视图来显示和选择每个用户的角色,而现在,这一功能已开箱即用,其方式与我们以前不得不自己做的方式非常相似。然而,这意味着我们需要确保使用预构建的管理员用户初始化应用程序。
注意 IdentityConfig 文件
在 Identity Samples 项目中,数据库初始化和播种在 App_Start => IdentityConfig.cs 文件中处理。目前,我们不需要对该文件进行任何更改。但是,请注意 ApplicationDbInitializer
类,在该类中我们定义了一个初始管理员用户、一个初始角色以及其他一些数据库配置项
IdentityConfig.cs 中的 ApplicationDbInitializer 类
public class ApplicationDbInitializer
: DropCreateDatabaseIfModelChanges<ApplicationDbContext>
{
protected override void Seed(ApplicationDbContext context) {
InitializeIdentityForEF(context);
base.Seed(context);
}
//Create User=Admin@Admin.com with password=Admin@123456 in the Admin role
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 IdentityRole(roleName);
var roleresult = roleManager.Create(role);
}
var user = userManager.FindByName(name);
if (user == null) {
user = new ApplicationUser { UserName = name, Email = name };
var result = userManager.Create(user, password);
result = userManager.SetLockoutEnabled(user.Id, false);
}
// Add user admin to Role Admin if not already added
var rolesForUser = userManager.GetRoles(user.Id);
if (!rolesForUser.Contains(role.Name)) {
var result = userManager.AddToRole(user.Id, role.Name);
}
}
}
另请注意,在上面的默认代码中,ApplicationDbInitializer
类派生自 DropCreateDatabaseIfModelChanges<ApplicationDbContext>
。顾名思义,这只会当模型更改影响数据库架构时才创建新数据库,替换旧数据库。通常,这已足够。然而,有时每次运行项目时都从一个全新的数据库开始是很方便的。在这种情况下,我们可以简单地将类声明更改为派生自 DropCreateDatabaseAlways<ApplicationDbContext>
,顾名思义,这将导致每次应用程序运行时都初始化一个全新的数据库。
目前,我们只需要了解 InitializeIdentityForEF()
方法中定义的默认管理员用户,以便知道如何首次登录。
运行已进行 IdentityUser 修改的项目
到目前为止,我们所做的只是向 ApplicationUser 模型的现有实现添加了一些新属性,并更新了相应的 ViewModel、View 和 Controller。然而,以这种方式扩展 IdentityUser 所需的就只有这些。项目现在应该可以运行,并且我们添加的地址属性应该在我们的应用程序中得到正确体现。
如果我们运行项目,使用 IdentityConfig.cs 文件中定义的用户登录,我们将拥有对用户和角色的管理员访问权限
已登录到 Identity Samples 应用程序
如果选择“用户管理”选项卡,我们会找到一个列表,其中(到目前为止)只包含种子管理员用户,事实上,地址信息是空白的。这是因为我们没有播种任何地址值
Identity Samples 项目的 Users Admin 选项卡,地址为空
现在我们可以导航到 编辑 视图,并使用一些地址信息更新我们的初始用户
编辑默认管理员用户并添加地址信息
输入地址信息并保存后,列表会更新,“用户管理” 索引 视图会显示更新后的信息
更新后的用户管理选项卡
如果我们导航到“管理员用户”选项卡上的“创建新用户”链接来创建新用户,或者如果我们注销并注册为新用户,情况也应该以类似的方式工作。
扩展基本的 IdentityUser 实现非常简单,看来 Identity Samples 项目的设计也考虑到了这一点。然而,当涉及到扩展或修改 IdentityRole
实现时,事情会变得复杂一些。
扩展 Identity Role
正如我们之前提到的,Identity 2.0 框架的设计考虑了很大的灵活性,通过在关键组件的基类中使用泛型类型和泛型方法。我们之前看到这如何创建了一组非常灵活的模型,但在它们相互依赖时,使用它们可能会有些棘手。
我们希望使用我们现有、已修改的 Identity Samples 项目,并进一步自定义 Identity Role 实现。请注意,Identity Samples 开箱即用不定义 ApplicationRole
类——该项目依赖于框架本身提供的基本IdentityRole由框架本身提供。
Identity Samples 项目只是使用了命名空间 Microsoft.AspNet.Identity.EntityFramework
中定义的 IdentityRole
类的默认实现。正如我们之前所见,IdentityRole
的默认定义如下
默认的 IdentityRole 实现
public class IdentityRole : IdentityRole<string, IdentityUserRole>
{
public IdentityRole()
{
base.Id = Guid.NewGuid().ToString();
}
public IdentityRole(string roleName) : this()
{
base.Name = roleName;
}
}
同样,正如我们之前讨论的,Identity 团队通过从 IdentityRole<TKey, TUserRole>
派生,将字符串和 Identity 框架类型 IdentityUserRole
作为类型参数传递给泛型类定义,创建了一个合理的默认实现。
如果我们希望扩展此实现以包含一些自定义属性,我们将需要定义自己的属性。我们可以通过直接从默认实现继承,并添加一个或多个自己的属性来完成。另外,我们可以从底层开始,通过从 IdentityRole<Tkey, TUserRole>
派生来创建自己的实现,但对于我们这里的目的,我们没有理由从抽象链的那么低级别开始。我们坚持使用默认的字符串键类型和基本的 IdentityUserRole
。
关于 IdentityRole 和 IdentityUserRole 的注意事项
让我们暂停一下,注意一个潜在的混淆点。Identity 框架定义了两个看似相似的类,IdentityRole
和 IdentityUserRole
。在最低的框架实现级别,它们都是实现特定接口的泛型类。如上所述,IdentityRole
的泛型实现如下所示
Identity Framework 2.0 中 IdentityRole 类的基本实现
public class IdentityRole<TKey, TUserRole> : IRole<TKey>
where TUserRole : IdentityUserRole<TKey>
{
public TKey Id
{
get
{
return JustDecompileGenerated_get_Id();
}
set
{
JustDecompileGenerated_set_Id(value);
}
}
public string Name
{
get;
set;
}
public ICollection<TUserRole> Users
{
get
{
return JustDecompileGenerated_get_Users();
}
set
{
JustDecompileGenerated_set_Users(value);
}
}
public IdentityRole()
{
this.Users = new List<TUserRole>();
}
}
同时,泛型基类 IdentityUserRole
如下所示
IdentityUserRole 的泛型基本实现
public class IdentityUserRole<TKey>
{
public virtual TKey RoleId
{
get;
set;
}
public virtual TKey UserId
{
get;
set;
}
public IdentityUserRole()
{
}
}
对于我们这里的目的,我们无需过多担心这些底层细节,只需注意 IdentityRole
和 IdentityUserRole
是两个不同的类,具有两个不同的目的。IdentityRole
代表我们应用程序和数据库中的一个实际角色实体,而 IdentityUserRole
代表用户和角色之间的关系。
由于名称相似,在编写代码时,尤其是在依赖 VS intellisense 时,很容易将两者混淆。当然,如果混淆了,编译器会告知您,但仍然最好认识到这种区别,尤其是在尝试更高级的自定义时。
向 Identity Samples 项目添加自定义角色
考虑到上述所有情况,让我们向我们的项目添加一个修改后的角色定义。为了与 IdentityUser 的项目实现(“ApplicationUser”)所使用的约定保持一致,我们将在 IdentityModels.cs 文件中添加一个名为 ApplicationRole
的类,该类继承自 IdentityRole
并实现一个自定义的 Description
属性
派生自默认 IdentityRole 类的自定义实现
public class ApplicationRole : IdentityRole
{
public ApplicationRole() : base() { }
public ApplicationRole(string name) : base(name) { }
public string Description { get; set; }
}
嗯,这还不错,对吧?好吧,我们才刚刚开始。由于我们不再使用默认的 IdentityRole 实现,如果要在 Identity Samples 项目中实际使用我们的自定义类,我们需要在许多地方引入一些非平凡的更改。
重新实现 RoleStore 和 ApplicationRoleManager
首先,如果我们再次查看 App_Start => IdentityConfig.cs 文件,我们会找到 ApplicationRoleManager
的类定义
Identity Samples 项目中 ApplicationRoleManager 的现有实现
public class ApplicationRoleManager : RoleManager<IdentityRole>
{
public ApplicationRoleManager(IRoleStore<IdentityRole,string> roleStore)
: base(roleStore)
{
}
public static ApplicationRoleManager Create(
IdentityFactoryOptions<ApplicationRoleManager> options, IOwinContext context)
{
return new ApplicationRoleManager(
new RoleStore<IdentityRole>(context.Get<ApplicationDbContext>()));
}
}
正如我们所看到的,这个类在很大程度上依赖于默认框架类型 IdentityRole
。我们需要将所有对 IdentityRole
类型的引用替换为我们自己的 ApplicationRole
实现。
修改后的 ApplicationRoleManager 类依赖于自定义 ApplicationRole
public class ApplicationRoleManager : RoleManager<ApplicationRole>
{
public ApplicationRoleManager(
IRoleStore<ApplicationRole,string> roleStore)
: base(roleStore)
{
}
public static ApplicationRoleManager Create(
IdentityFactoryOptions<ApplicationRoleManager> options, IOwinContext context)
{
return new ApplicationRoleManager(
new RoleStore<ApplicationRole>(context.Get<ApplicationDbContext>()));
}
}
另外,在我们的 ApplicationDbInitializer
类(也在 IdentityConfig.cs 文件中)的 InitializeDatabaseForEF()
方法中,我们需要初始化一个新的 ApplicationRole
,而不是新的 IdentityRole
在数据库设置中初始化 ApplicationRole
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 };
var result = userManager.Create(user, password);
result = userManager.SetLockoutEnabled(user.Id, false);
}
// Add user admin to Role Admin if not already added
var rolesForUser = userManager.GetRoles(user.Id);
if (!rolesForUser.Contains(role.Name)) {
var result = userManager.AddToRole(user.Id, role.Name);
}
}
更新 Roles Admin 控制器上的 Create 方法
与 InitializeDatabaseForEF()
方法类似,我们还需要在 RolesAdminController
的 Create()
方法中正确初始化 ApplicationRole
的新实例,而不是 IdentityRole
更新 RolesAdminController 上的 Create 方法
public async Task<ActionResult> Create(RoleViewModel roleViewModel)
{
if (ModelState.IsValid)
{
// Initialize ApplicationRole instead of IdentityRole:
var role = new ApplicationRole(roleViewModel.Name);
var roleresult = await RoleManager.CreateAsync(role);
if (!roleresult.Succeeded)
{
ModelState.AddModelError("", roleresult.Errors.First());
return View();
}
return RedirectToAction("Index");
}
return View();
}
现在我们需要确保我们可以在视图中消费我们改进后的角色实现。正如我们对修改后的 ApplicationUser 类所做的那样,现在我们需要在 ViewModel 和 View 中适应或新的角色实现,并确保我们在控制器和视图之间适当地传递属性数据。
向 RoleViewModel 添加扩展属性
在 AdminViewModels.cs 文件中,我们需要通过添加新的 Description
属性来更新 RoleViewModel
的定义
更新后的 RoleViewModel
public class RoleViewModel
{
public string Id { get; set; }
[Required(AllowEmptyStrings = false)]
[Display(Name = "RoleName")]
public string Name { get; set; }
public string Description { get; set; }
}
接下来,我们需要确保相应的视图将 Description
属性可用于显示和/或表单输入。
更新 Roles Admin Create.cshtml 视图
我们需要更新的视图位于 VS 解决方案资源管理器中的 Views => RolesAdmin 文件夹中。
与我们对用户管理视图所做的类似,我们需要使 Description
属性可用,以便管理员在创建新角色时,也可以输入并保存描述。将表单元素添加到 Views => RolesAdmin => Create.cshtml 视图中,用于 Description
属性
角色管理更新后的 Create.cshtml 视图
@model IdentitySample.Models.RoleViewModel
@{
ViewBag.Title = "Create";
}
<h2>Create.</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Role.</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.TextBoxFor(model => model.Name, new { @class = "form-control" })
@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.TextBoxFor(model => model.Description, new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.Description)
</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")
}
更新 Roles Admin Edit.cshtml 视图
接下来,我们对 Views => RolesAdmin => Edit.cshtml 视图进行类似修改
修改后的 Roles Admin Edit.cshtml 视图
@model IdentitySample.Models.RoleViewModel
@{
ViewBag.Title = "Edit";
}
<h2>Edit.</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Roles.</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.TextBoxFor(model => model.Name, new { @class = "form-control" })
@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.TextBoxFor(model => model.Description, new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.Description)
</div>
</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")
}
更新 Roles Admin Index.cshtml 视图
与创建和编辑视图不同,索引、删除和详细信息视图需要进行一些额外的调整。请注意,以其现有形式,这三个视图模板期望 List<IdentityRole>
的实例作为从控制器传递的模型。我们需要更改代码的第一行,以期望 IEnumerable<ApplicationRole>
。然后,我们对表头和表行元素进行相对简单的添加,以显示 Description
属性
更新后的 Roles Admin Index.cshtml 视图
@model IEnumerable<IdentitySample.Models.ApplicationRole>
@{
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>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
Delete.cshtml 和 Details.cshtml 视图可以用类似的方式修改,为了简洁起见,我们在此处不做演示。但是请注意,对于 Index 视图,视图期望一个 IEnnumerable<ApplicationRole>
,而 Details 和 Delete 视图将期望一个 ApplicationRole
的单个实例。
更新角色管理控制器
为了使所有这些工作正常进行,我们现在需要修改 RolesAdminController
上的 Create()
和 Edit()
方法,以便正确的数据在相应的视图之间传递,并正确地持久化到后端存储。
更新 Roles Admin 控制器上的 Create 方法
创建方法接收 ApplicationRole 的实例作为表单数据,并将新角色持久化到数据库。我们在这里需要做的就是确保新的描述数据也已保存
角色管理控制器上更新后的 Create 方法
[HttpPost]
public async Task<ActionResult> Create(RoleViewModel roleViewModel)
{
if (ModelState.IsValid)
{
var role = new ApplicationRole(roleViewModel.Name);
// Save the new Description property:
role.Description = roleViewModel.Description;
var roleresult = await RoleManager.CreateAsync(role);
if (!roleresult.Succeeded)
{
ModelState.AddModelError("", roleresult.Errors.First());
return View();
}
return RedirectToAction("Index");
}
return View();
}
更新 Roles Admin 控制器上的 Edit 方法
Edit()
方法还有一些额外的项目需要处理。首先,我们需要在 GET 请求中用当前数据填充表单,然后,当 POST 返回时,我们需要绑定适当的表单数据,并确保新的 Description
属性与其他所有内容一起保存。
更新 Roles Admin 控制器上的 Edit 方法
public async Task<ActionResult> Edit(string id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var role = await RoleManager.FindByIdAsync(id);
if (role == null)
{
return HttpNotFound();
}
RoleViewModel roleModel = new RoleViewModel
{
Id = role.Id,
Name = role.Name
};
// Update the new Description property for the ViewModel:
roleModel.Description = role.Description;
return View(roleModel);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit([Bind(
Include = "Name,Id,Description")]
RoleViewModel roleModel)
{
if (ModelState.IsValid)
{
var role = await RoleManager.FindByIdAsync(roleModel.Id);
role.Name = roleModel.Name;
// Update the new Description property:
role.Description = roleModel.Description;
await RoleManager.UpdateAsync(role);
return RedirectToAction("Index");
}
return View();
}
运行已进行角色修改的项目
如果我们仔细更新了所有视图和控制器,那么现在应该能够看到我们扩展的角色在运行。如果我们运行项目,登录并导航到角色管理选项卡,我们会找到一个包含单个角色的列表。此时描述字段为空,因为我们没有在种子方法中为预定义的初始角色添加描述。
启动时的角色管理选项卡
如果选择编辑现有角色,我们可以输入新的描述
编辑角色视图 - 数据输入
保存新条目后,我们可以看到索引视图现在显示角色描述
编辑后的角色管理索引视图
总结
在本文中,我们探讨了如何在 Identity 2.0 框架中扩展和修改关键的 IdentityUser 和 IdentityRole 组件。我们是在 Identity Samples 项目的上下文中完成的,该项目为学习如何实现 Identity 2.0 以进行基本身份验证和授权目的提供了强大的平台,也为您自己的 Web 应用程序的 Identity 部分奠定了坚实的基础。
需要记住的事项有
Identity Samples 项目是 Alpha 版本,可能会随着时间而演变——将来可能会有影响本文具体内容的更改。
Identity 2.0 RTM 为 ASP.NET 平台带来了实质性的灵活性和一系列额外的功能,这些功能在此之前一直明显缺失。我们在这里只触及了皮毛。
Identity 2.0 和 Identity Samples 项目在更灵活、更复杂的基础模型之上提供了一个简化的抽象。定制化的程度更高,需要的权宜之计更少。但是,必要的步骤不一定立即显而易见。由于组件的相互依赖性,泛型类型组件的框架在定制时需要额外考虑。
作为开发人员,我们有能力深入探索并找出问题。在最大限度地发挥 Identity 2.0 对我们应用程序的益处并普遍学习新框架的过程中,我们将完成很多这样的工作。
其他资源和感兴趣的项目
- Github 上面创建的项目源代码
- ASP.NET MVC 和 Identity 2.0:了解基础知识
- ASP.NET Identity 2.0:设置账户验证和双因素授权
- ASP.NET Identity 账户确认和密码恢复