【ASP.NET Identity系列教程(一)】ASP.NET Identity入门

注:本文是【ASP.NET Identity系列教程】的第一篇。本系列教程详细、完整、深入地介绍了微软的ASP.NET Identity技术,描述了如何运用ASP.NET Identity实现应用程序的用户管理,以及实现应用程序的认证与授权等相关技术,译者希望本系列教程能成为掌握ASP.NET Identity技术的一份完整而有价值的资料。读者若是能够按照文章的描述,一边阅读、一边实践、一边理解,定能有意想不到的巨大收获!希望本系列博文能够得到广大园友的高度推荐





13 Getting Started with Identity
13 Identity入门

Identity is a new API from Microsoft to manage users in ASP.NET applications. The mainstay for user management in recent years has been ASP.NET Membership, which has suffered from design choices that were reasonable when it was introduced in 2005 but that have aged badly. The biggest limitation is that the schema used to store the data worked only with SQL Server and was difficult to extend without re-implementing a lot of provider classes. The schema itself was overly complex, which made it harder to implement changes than it should have been.
Identity是微软在ASP.NET应用程序中管理用户的一个新的API。近年来用户管理的基石一直是ASP.NET的Membership。Membership在2005年推出时还算是一个合理的选择,但目前看来已经严重过时了。它最大的限制是用以存储数据的架构(Database Schema)只能使用SQL Server,而且难以扩展,除非重新实现大量的提供器类。其数据架构本身也过于复杂,使之比理论上还要难以实现修改。

Microsoft made a couple of attempts to improve Membership prior to releasing Identity. The first was known as simple membership, which reduced the complexity of the schema and made it easier to customize user data but still needed a relational storage model. The second attempt was the ASP.NET universal providers, which I used in Chapter 10 when I set up SQL Server storage for session data. The advantage of the universal providers is that they use the Entity Framework Code First feature to automatically create the database schema, which made it possible to create databases where access to schema management tools wasn't possible, such as the Azure cloud service. But even with the improvements, the fundamental issues of depending on relational data and difficult customizations remained.
在发布Identity之前,微软曾做过两次改善Membership的尝试。第一个尝试称为Simple Membership(简单成员),它降低了数据库架构的复杂性,并使之易于定制用户数据,但仍然需要关系型存储模型。第二个尝试是ASP.NET的Universal Providers(通用提供器),第10章在为会话数据建立SQL Server存储库时曾用过它。Universal Providers的好处是,这些提供器使用了Entity Framework的Code First特性,能够自动地创建数据库架构,使之能够在架构管理工具无法访问的情况下,例如Azure云服务,也能够创建数据库。但即使有了改善,其依赖于关系型数据以及难以定制等根本问题仍然存在。

To address both problems and to provide a more modern user management platform, Microsoft has replaced Membership with Identity. As you'll learn in this chapter and Chapters 14 and 15, ASP.NET Identity is flexible and extensible, but it is immature, and features that you might take for granted in a more mature system can require a surprising amount of work.
为了解决这两个问题并提供一个更现代的用户管理平台,微软用Identity取代了Membership。正如将在本章以及第14、15章所了解到的,ASP.NET Identity灵活且可扩展,但它仍不成熟,你在一些更成熟的系统中能够获得的特性,可能需要超常的工作量。

Microsoft has over-compensated for the inflexibility of Membership and made Identity so open and so adaptable that it can be used in just about any way—just as long as you have the time and energy to implement what you require.
微软已经完全弥补了Membership的不灵活性,使Identity十分开放和广泛适应,几乎能够以任何方式进行使用——只要你有时间有能力做出你所需要的实现即可。

In this chapter, I demonstrate the process of setting up ASP.NET Identity and creating a simple user administration tool that manages individual user accounts that are stored in a database.
在本章中,我会演示建立ASP.NET Identity的过程,并创建一个简单的用户管理工具,用以管理存储在数据库中的个别用户账号。

ASP.NET Identity supports other kinds of user accounts, such as those stored using Active Directory, but I don't describe them since they are not used that often outside corporations (where Active Directive implementations tend to be so convoluted that it would be difficult for me to provide useful general examples).
ASP.NET Identity还支持其他类型的用户账号,例如用Active Directory(活动目录)存储的账号,但我不会对其进行描述,因为这种账号通常不会用于公司的外部(这种场合的Active Directory实现往往很复杂,我难以提供有用的通用示例)。

In Chapter 14, I show you how to perform authentication and authorization using those user accounts, and in Chapter 15, I show you how to move beyond the basics and apply some advanced techniques. Table 13-1 summarizes this chapter.
在第14章中,我将演示如何用这些用户账号进行认证与授权,第15章将演示如何进入高级论题,运用一些高级技术。表13-1是本章概要。

Table 13-1. Chapter Summary
表13-1. 本章概要
Problem
问题
Solution
解决方案
Listing
清单号
Install ASP.NET Identity.
安装ASP.NET Identity
Add the NuGet packages and define a connection string and an OWIN start class in the Web.config file.
添加NuGet包,并在Web.config文件中定义一个链接字符串和一个OWIN启动类
1–4
Prepare to use ASP.NET Identity.
使用ASP.NET Identity的准备
Create classes that represent the user, the user manager, the database context, and the OWIN start class.
创建表示用户、用户管理器、数据库上下文的类,以及OWIN类
5–8
Enumerate user accounts.
枚举用户账号
Use the Users property defined by the user manager class.
使用由用户管理器类定义的Users属性
9, 10
Create user accounts.
创建用户账号
Use the CreateAsync method defined by the user manager class.
使用由用户管理器类定义的CreateAsync方法
11–13
Enforce a password policy.
强制口令策略
Set the PasswordValidator property defined by the user manager class, either using the built-in PasswordValidator class or using a custom derivation.
设置由用户管理器类定义的PasswordValidator属性,既可以使用内建的PasswordValidator类,也可以使用自定义的派生类。
14–16
Validate new user accounts.
验证新的用户账号
Set the UserValidator property defined by the user manager class, either using the built-in UserValidator class or using a custom derivation.
设置由用户管理器类定义的UserValidator属性,既可以使用内建的UserValidator类,也可以使用自定义的派生类。
17–19
Delete user accounts.
删除用户账号
Use the DeleteAsync method defined by the user manager class.
使用由用户管理器定义的DeleteAsync方法
20–22
Modify user accounts.
修改用户账号
Use the UpdateAsync method defined by the user manager class.
使用由用户管理器类定义的UpdateAsync方法
23–24

13.1 Preparing the Example Project
13.1 准备示例项目

I created a project called Users for this chapter, following the same steps I have used throughout this book. I selected the Empty template and checked the option to add the folders and references required for an MVC application. I will be using Bootstrap to style the views in this chapter, so enter the following command into the Visual Studio Package Manager Console and press Enter to download and install the NuGet package:
本章根据本书一直采用的同样步骤创建了一个名称为Users的项目。在创建过程中选择了“Empty(空)” 模板,并在“Add the folders and references(添加文件夹和引用)”中选中了“MVC”复选框。本章将使用Bootstrap来设置视图的样式,因此在Visual Studio的“Package Manager Console(包管理器控制台)”中输入以下命令,并按回车,下载并安装这个NuGet包。

Install-Package -version 3.0.3 bootstrap

I created a Home controller to act as the focal point for the examples in this chapter. The definition of the controller is shown in Listing 13-1. I'll be using this controller to describe details of user accounts and data, and the Index action method passes a dictionary of values to the default view via the View method.
我创建了Home控制器,以作为本章示例的焦点。该控制器的定义如清单13-1所示。此控制器将用来描述用户账号的细节和数据,Index动作方法通过View方法给默认视图传递了一个字典值。

Listing 13-1. The Contents of the HomeController.cs File
清单13-1. HomeController.cs文件的内容

using System.Web.Mvc;
using System.Collections.Generic;
namespace Users.Controllers {
public class HomeController : Controller {
public ActionResult Index() { Dictionary<string, object> data = new Dictionary<string, object>(); data.Add("Placeholder", "Placeholder"); return View(data); } } }

I created a view by right-clicking the Index action method and selecting Add View from the pop-up menu. I set View Name to Index and set Template to Empty (without model). Unlike the examples in previous chapters, I want to use a common layout for this chapter, so I checked the Use a Layout Page option. When I clicked the Add button, Visual Studio created the Views/Shared/_Layout.cshtml and Views/Home/Index.cshtml files. Listing 13-2 shows the contents of the _Layout.cshtml file.
通过右击Index动作方法,并从弹出菜单选择“Add View(添加视图)”,我创建了一个视图。将“View Name(视图名称)”设置为“Index”,并将“Template(模板)”设置为“空(无模型)”。与前面几章的示例不同,本章希望使用一个通用的布局,于是选中了“Use a Layout Page(使用布局页面)”复选框。点击“Add(添加)”按钮后,Visual Studio创建了Views/Shared/_Layout.cshtmlViews/Home/Index.cshtml文件。清单13-2显示了_Layout.cshtml文件的内容。

Listing 13-2. The Contents of the _Layout.cshtml File
清单13-2. _Layout.cshtml文件的内容

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    <link href="~/Content/bootstrap.min.css" rel="stylesheet" />
    <link href="~/Content/bootstrap-theme.min.css" rel="stylesheet" />
    <style>
        .container { padding-top: 10px; }
        .validation-summary-errors { color: #f00; }
    </style>
</head>
<body class="container">
    <div class="container">
        @RenderBody()
    </div>
</body>
</html>

Listing 13-3 shows the contents of the Index.cshtml file.
清单13-3显示了Index.cshtml文件的内容。

Listing 13-3. The Contents of the Index.cshtml File
清单13-3. Index.cshtml文件的内容

@{
    ViewBag.Title = "Index";
}
<div class="panel panel-primary"> <div class="panel-heading">User Details</div> <table class="table table-striped"> @foreach (string key in Model.Keys) { <tr> <th>@key</th> <td>@Model[key]</td> </tr> } </table> </div>

To test that the example application is working, select Start Debugging from the Visual Studio Debug menu and navigate to the /Home/Index URL. You should see the result illustrated by Figure 13-1.
为了测试该应用程序示例能够工作,从Visual Studio的“Debug(调试)”菜单中选择“Start Debugging(启动调试)”,并导航到/Home/Index网址,便可以看到如图13-1所示的结果。

图13-1

Figure 13-1. Testing the example application
图13-1. 测试示例应用程序

13.2 Setting Up ASP.NET Identity
13.2 建立ASP.NET Identity

For most ASP.NET developers, Identity will be the first exposure to the Open Web Interface for .NET (OWIN). OWIN is an abstraction layer that isolates a web application from the environment that hosts it. The idea is that the abstraction will allow for greater innovation in the ASP.NET technology stack, more flexibility in the environments that can host ASP.NET applications, and a lighter-weight server infrastructure.
对于大多数ASP.NET开发者而言,Identity将是第一个暴露给OWIN(Open Web Interface for .NET——.NET开放Web接口)的组件。OWIN是一个将Web应用程序从托管它的环境中独立出来的抽象层。其思想是这个独立出来的抽象层能够使ASP.NET技术堆栈有更大的创新,使托管ASP.NET应用程序的环境有更多的灵活性,并可以是轻量级的服务器架构。

OWIN is an open standard (which you can read at http://owin.org/spec/owin-1.0.0.html). Microsoft has created Project Katana, its implementation of the OWIN standard and a set of components that provide the functionality that web applications require. The attraction to Microsoft is that OWIN/Katana isolates the ASP.NET technology stack from the rest of the .NET Framework, which allows a greater rate of change.
OWIN是一个开放标准(参阅http://owin.org/spec/owin-1.0.html)。微软已经创建了Katana项目,该项目是OWIN标准的实现,并提供了一组组件,这些组件提供了Web应用程序所需的功能。让微软感兴趣的是OWIN/Katana将ASP.NET技术堆栈从.NET框架的其余部分独立了出来,这带来了更大的修改速率(译者对OWIN还没太了解,这可能是指若按照OWIN的方式开发应用程序,可以使应用程序更易于修改——译者注)。

OWIN developers select the services that they require for their application, rather than consuming an entire platform as happens now with ASP.NET. Individual services—known as middleware in the OWIN terminology—can be developed at different rates, and developers will be able to choose between providers for different services, rather than being tied to a Microsoft implementation.
OWIN开发人员为他们的应用程序选择所需的服务,而不是像现在这样单纯地使用ASP.NET平台。个别服务——在OWIN术语中称为“Middleware(中间件)”——可能会有不同的开发速率,而开发人员将能够在不同的服务提供商之间进行选择,而不是被绑定在微软实现上。

There is a lot to like about the direction that OWIN and Katana are heading in, but it is in the early days, and it will be some time before it becomes a complete platform for ASP.NET applications. As I write this, it is possible to build Web API and SignalR applications without needing the System.Web namespace or IIS to process requests, but that's about all. The MVC framework requires the standard ASP.NET platform and will continue to do so for some time to come.
OWIN和Katana有很多喜欢发展的方向,但它仍处于早期时期,还需要一段时间才能成为ASP.NET应用程序的完整平台。在我编写本书的时候,它已可以在不需要System.Web命名空间或者IIS处理请求的情况下,建立Web API和SignalR应用程序了。MVC框架需要标准的ASP.NET平台,因此在一段时间内仍将沿用现在的做法。

The ASP.NET platform and IIS are not going away. Microsoft has been clear that it sees one of the most attractive aspects of OWIN as allowing developers more flexibility in which middleware components are hosted by IIS, and Project Katana already has support for the System.Web namespaces. OWIN and Katana are not the end of ASP.NET—rather, they represent an evolution where Microsoft allows developers more flexibility in how ASP.NET applications are assembled and executed.
ASP.NET平台以及IIS不会消失。微软已经清楚地看到,OWIN最有吸引力的一个方面是开发者具有了更大的灵活性,中间组件可以由IIS来托管,而Katana项目已经实现了对System.Web命名空间的支持。OWIN和Katana不是ASP.NET的终结——而是预示着一种变革,微软让开发人员能够在ASP.NET应用程序的编译和执行方面更加灵活。

Tip Identity is the first major ASP.NET component to be delivered as OWIN middleware, but it won't be the last. Microsoft has made sure that the latest versions of Web API and SignalR don't depend on the System.Web namespaces, and that means that any component intended for use across the ASP.NET family of technologies has to be delivered via OWIN. I get into more detail about OWIN in my Expert ASP.NET Web API 2 for MVC Developers book, which will be published by Apress in 2014.
提示:Identity是作为OWIN中间件而交付的第一个主要的ASP.NET组件,但这不是最后一个。微软已经保证,Web API和SignalR的最新版本不会依赖于System.Web命名空间,这意味着,打算交叉使用ASP.NET家族技术的任何组件,都必须通过OWIN实行交付。关于OWIN,在我的Expert ASP.NET Web API 2 for MVC Developers一书中有更详细的论述,该书已由Apress于2014年出版。

OWIN and Katana won't have a major impact on MVC framework developers for some time, but changes are already taking effect—and one of these is that ASP.NET Identity is implemented as an OWIN middleware component. This isn't ideal because it means that MVC framework applications have to mix OWIN and traditional ASP.NET platform techniques to use Identity, but it isn't too burdensome once you understand the basics and know how to get OWIN set up, which I demonstrate in the sections that follow.
OWIN和Katana在一段时间内还不会对MVC框架开发人员发生重大冲击,但变化正在发生——其中之一便是ASP.NET Identity要作为OWIN中间件而实现。这种情况不太理想,因为这意味着,MVC框架的应用程序为了使用Identity,必须将OWIN和传统的ASP.NET平台混搭在一起,这太烦琐了,在你理解了基本概念并且知道如何建立OWIN(以下几小节演示)之后,便会明白。

13.2.1 Creating the ASP.NET Identity Database
13.2.1 创建ASP.NET Identity数据库

ASP.NET Identity isn't tied to a SQL Server schema in the same way that Membership was, but relational storage is still the default—and simplest—option, and it is the one that I will be using in this chapter. Although the NoSQL movement has gained momentum in recent years, relational databases are still the mainstream storage choice and are well-understood in most development teams.
ASP.NET Identity并未像Membership那样,被绑定到SQL Server架构,但关系型存储仍是默认的,而且是最简单的选择,这也是本章中将使用的形式。虽然近年来出现了NoSQL运动势头,但关系型数据库仍然是主要的存储选择,而且大多数开发团队都有良好理解。

ASP.NET Identity uses the Entity Framework Code First feature to automatically create its schema, but I still need to create the database into which that schema—and the user data—will be placed, just as I did in Chapter 10 when I created the database for session state data (the universal provider that I used to manage the database uses the same Code First feature).
ASP.NET Identity使用Entity Framework的Code First特性自动地创建它的数据架构(Schema),但我仍然需要创建一个用来放置此数据架构以及用户数据的数据库,就像第10章所做的那样,当时为会话状态数据创建了数据库(用来管理数据库的通用提供器同样使用了Code First特性)。

Tip You don't need to understand how Entity Framework or the Code First feature works to use ASP.NET Identity.
提示:为了使用ASP.NET Identity,不一定要理解Entity Framework或Code First特性的工作机制。

As in Chapter 10, I will be using the localdb feature to create my database. As a reminder, localdb is included in Visual Studio and is a cut-down version of SQL Server that allows developers to easily create and work with databases.
正如第10章那样,我将使用localdb特性来创建数据库。要记住的是,localdb是包含在Visual Studio之中的,而且是一个简化版的SQL Server,能够让开发者方便地创建和使用数据库。

Select SQL Server Object Explorer from the Visual Studio View menu and right-click the SQL Server object in the window that appears. Select Add SQL Server from the pop-up menu, as shown in Figure 13-2.
从Visual Studio的“View(视图)”菜单中选择“SQL Server Object Explorer(SQL Server对象资源管理器)”,并在所出现的窗口中右击“SQL Server”对象。从弹出菜单选择“Add SQL Server(添加SQL Server)”,如图13-2所示。

图13-2

Figure 13-2. Creating a new database connection
图13-2. 创建一个新的数据库连接

Visual Studio will display the Connect to Server dialog. Set the server name to (localdb)\v11.0, select the Windows Authentication option, and click the Connect button. A connection to the database will be established and shown in the SQL Server Object Explorer window. Expand the new item, right-click Databases, and select Add New Database from the pop-up window, as shown in Figure 13-3.
Visual Studio将显示“Connect to Server(连接到服务器)”对话框,将服务器名称设置为(localdb)\v11.0,选择“Windows Authentication(Windows认证)”选项,点击“Connect(连接)”按钮。这将建立一个数据库连接,并显示在“SQL Server对象资源管理器”窗口中。展开这个新项,右击“Databases(数据库)”,并从弹出菜单选择“Add New Database(添加新数据库)”,如图13-3所示。

图13-3

Figure 13-3. Adding a new database
图13-3. 添加新数据库

Set the Database Name option to IdentityDb, leave the Database Location value unchanged, and click the OK button to create the database. The new database will be shown in the Databases section of the SQL connection in the SQL Server Object Explorer.
将“Database Name(数据库名称)”选项设置为“IdentityDb”,不用改变“Database Location(数据库位置)”的值,点击OK按钮创建此数据库。这个新数据库将出现在“SQL Server对象资源管理器”中“SQL连接”的“数据库”小节中。

13.2.2 Adding the Identity Packages
13.2.2 添加Identity包

Identity is published as a set of NuGet packages, which makes it easy to install them into any project. Enter the following commands into the Package Manager Console:
Identity是作为一组NuGet包发布的,因此易于将其安装到任何项目。请在“Package Manager Console(包管理器控制台)”中输入以下命令:

Install-Package Microsoft.AspNet.Identity.EntityFramework –Version 2.0.0(2.2.1)
Install-Package Microsoft.AspNet.Identity.OWIN -Version 2.0.0(2.2.1)
Install-Package Microsoft.Owin.Host.SystemWeb -Version 2.1.0(3.0.1)

Visual Studio can create projects that are configured with a generic user account management configuration, using the Identity API. You can add the templates and code to a project by selecting the MVC template when creating the project and setting the Authentication option to Individual User Accounts. I don't use the templates because I find them too general and too verbose and because I like to have direct control over the contents and configuration of my projects. I recommend you do the same, not least because you will gain a better understanding of how important features work, but it can be interesting to look at the templates to see how common tasks are performed.
Visual Studio能够创建一些使用Identity API的项目,这种项目以“泛型用户账号管理配置”进行配置。你可以给项目添加一些模板和代码,只需在创建项目时选择MVC模板,并将认证选项设置为Individual User Accounts(个别用户账号)。我没有使用这种模板,因为我发现它们太普通,也太混乱,而且我喜欢对我项目中的内容和配置有直接的控制。我建议你也这么做,这不仅让你能够对重要特性的工作机制获得更好的理解,而且,考察模板如何执行常规任务也是有趣的。

13.2.3 Updating the Web.config File
13.2.3 更新Web.config文件

Two changes are required to the Web.config file to prepare a project for ASP.NET Identity. The first is a connection string that describes the database I created in the previous section. The second change is to define an application setting that names the class that initializes OWIN middleware and that is used to configure Identity. Listing 13-4 shows the changes I made to the Web.config file. (I explained how connection strings and application settings work in Chapter 9.)
为了做好项目使用ASP.NET Identity的准备,需要在Web.config文件中做两处修改。第一处是连接字符串,它描述了我在上一小节中创建的数据库。第二处修改是定义一个应用程序设置,它命名对OWIN中间件进行初始化的类,并将它用于配置Identity。清单显示了对Web.config文件的修改(第9章已经解释过连接字符串和应用程序设置)。

Listing 13-4. Preparing the Web.config File for ASP.NET Identity
清单13-4. 为ASP.NET Identity准备Web.config文件

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <configSections>
        <section name="entityFramework"
            type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection,
            EntityFramework, Version=6.0.0.0, Culture=neutral,
            PublicKeyToken=b77a5c561934e089" requirePermission="false" />
    </configSections> 
<connectionStrings> <add name="IdentityDb" providerName="System.Data.SqlClient" connectionString="Data Source=(localdb)\v11.0; Initial Catalog=IdentityDb; Integrated Security=True; Connect Timeout=15; Encrypt=False;TrustServerCertificate=False; MultipleActiveResultSets=True"/> </connectionStrings>
<appSettings> <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" /> <add key="owin:AppStartup" value="Users.IdentityConfig" /> </appSettings> <system.web> <compilation debug="true" targetFramework="4.5.1" /> <httpRuntime targetFramework="4.5.1" /> </system.web> <entityFramework> <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework"> <parameters> <parameter value="v11.0" /> </parameters> </defaultConnectionFactory> <providers> <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" /> </providers> </entityFramework> </configuration>

Caution Make sure you put the connectionString value on a single line. I had to break it over several lines to make the listing fit on the page, but ASP.NET expects a single, unbroken string. If in doubt, download the source code that accompanies this book, which is freely available from www.apress.com.
警告:要确保将connectionString的值放在一行中,这里把它给断开了,是为了让清单适应页面,但ASP.NET要求是单一无断行的字符串。如果有疑问,请下载本书的伴随代码,下载地址:www.apress.com

OWIN defines its own application startup model, which is separate from the global application class that I described in Chapter 3. The application setting, called owin:AppStartup, specifies a class that OWIN will instantiate when the application starts in order to receive its configuration.
OWIN定义了它自己的应用程序启动模型,与第3章所描述的全局应用程序类是分开的。上述的应用程序设置,名称为owin:AppStartup,指定了应用程序启动时,将由OWIN进行实例化的类,目的是接受它的配置。

Tip Notice that I have set the MultipleActiveResultSets property to true in the connection string. This allows the results from multiple queries to be read simultaneously, which I rely on in Chapter 14 when I show you how to authorize access to action methods based on role membership.
提示:注意,我已经在连接字符串中将MultipleActiveResultSets属性(多活动结果集)设置为true,这样可以通过同时读取的多个查询来形成结果,第14章便依赖于这种方式,那时会演示如何根据角色成员来授权访问动作方法。

13.2.4 Creating the Entity Framework Classes
13.2.4 创建Entity Framework类

If you have used Membership in projects, you may be surprised by just how much initial preparation is required for ASP.NET Identity. The extensibility that Membership lacked is readily available in ASP.NET Identity, but it comes with a price of having to create a set of implementation classes that the Entity Framework uses to manage the database. In the sections that follow, I’ll show you how to create the classes needed to get Entity Framework to act as the storage system for ASP.NET Identity.
如果你曾在项目使用过Membership,可能会感觉奇怪,ASP.NET Identity需要的初始化准备怎么这么多。在ASP.NET Identity中可以轻易地使用Membership所缺乏的可扩展性,但其代价是需要创建一组实现类,由Entity Framework用于管理数据库。在以下小节中,我将演示如何创建所需要的这些类,以便让Entity Framework将它们用于ASP.NET Identity的存储系统。

1. Creating the User Class
1. 创建用户类

The first class to define is the one that represents a user, which I will refer to as the user class. The user class is derived from IdentityUser, which is defined in the Microsoft.AspNet.Identity.EntityFramework namespace. IdentityUser provides the basic user representation, which can be extended by adding properties to the derived class, which I describe in Chapter 15. Table 13-2 shows the built-in properties that IdentityUser defines, which are the ones I will be using in this chapter.
第一个要定义的类是表现一个用户的类,我将它称为“User Class(用户类)”。这个用户类派生于IdentityUser,它是在Microsoft.AspNet.Identity.EntityFramework命名空间中定义的。IdentityUser提供了基本的用户表示,可以通过在它派生的类中添加属性的办法,对这个类进行扩展,我会在第15章中对此进行描述。表13-2列出了IdentityUser所定义的内建属性(现在流行将“内建(Built-in)”说成“内置”,但我不会人云亦云,这两者在含义上是有区别的,“内建”完全是自己创建的,而“内置”有可能是别人做的东西拿来放入其中的——译者注),本章将使用这些属性。

Table 13-2. The Properties Defined by the IdentityUser Class
表13-2. IdentityUser类定义的属性
Name
名称
Description
描述
Claims Returns the collection of claims for the user, which I describe in Chapter 15
返回用户的声明集合,关于声明(Claims)将在第15章描述
Email Returns the user's e-mail address
返回用户的E-mail地址
Id Returns the unique ID for the user
返回用户的唯一ID
Logins Returns a collection of logins for the user, which I use in Chapter 15
返回用户的登录集合,将在第15章中使用
PasswordHash Returns a hashed form of the user password, which I use in the “Implementing the Edit Feature” section
返回哈希格式的用户口令,在“实现Edit特性”中会用到它
Roles Returns the collection of roles that the user belongs to, which I describe in Chapter 14
返回用户所属的角色集合,将在第14章描述
PhoneNumber Returns the user's phone number
返回用户的电话号码
SecurityStamp Returns a value that is changed when the user identity is altered, such as by a password change
返回变更用户标识时被修改的值,例如被口令修改的值
UserName Returns the username
返回用户名

Tip The classes in the Microsoft.AspNet.Identity.EntityFramework namespace are the Entity Framework–specific concrete implementations of interfaces defined in the Microsoft.AspNet.Identity namespace. IdentityUser, for example, is the implementation of the IUser interface. I am working with the concrete classes because I am relying on the Entity Framework to store my user data in a database, but as ASP.NET Identity matures, you can expect to see alternative implementations of the interfaces that use different storage mechanisms (although most projects will still use the Entity Framework since it comes from Microsoft).
提示:Microsoft.AspNet.Identity.EntityFramework命名空间中的类是, Microsoft.AspNet.Identity命名空间中所定义接口的Entity Framework专用的具体实现。例如,IdentityUser便是IUser接口的实现。我会使用这些具体类,因为我要依靠Entity Framework在数据库中存储我的用户数据,等到ASP.NET Identity变得成熟时,你可能会期望看到这些接口的其他实现,它们使用了不同的存储机制(当然,大多数项目仍然会使用Entity Framework,因为它来自于微软)。

What is important at the moment is that the IdentityUser class provides access only to the basic information about a user: the use's name, e-mail, phone, password hash, role memberships, and so on. If I want to store any additional information about the user, I have to add properties to the class that I derive from IdentityUser and that will be used to represent users in my application. I demonstrate how to do this in Chapter 15.
目前最重要的是这个IdentityUser类只提供了对用户基本信息的访问:用户名、E-mail、电话、哈希口令、角色成员等等。如果希望存储用户的各种附加信息,就需要在IdentityUser派生的类上添加属性,并将它用于表示应用程序中的用户,第15章中将演示其做法。

To create the user class for my application, I created a class file called AppUserModels.cs in the Models folder and used it to create the AppUser class, which is shown in Listing 13-5.
为了创建应用程序中的用户类,我在Models文件夹中创建了一个类文件,名称为AppUserModels.cs(注意,这个文件名称错了,应当是AppUser.cs),并用它创建了AppUser类,这个类如清单13-5所示。

Listing 13-5. The Contents of the AppUser.cs File
清单13-5. AppUser.cs文件的内容

using System;
using Microsoft.AspNet.Identity.EntityFramework; 
namespace Users.Models {
public class AppUser : IdentityUser { // additional properties will go here // 这里将放置附加属性 } }

That's all I have to do at the moment, although I'll return to this class in Chapter 15, when I show you how to add application-specific user data properties.
以上便是此刻要做的全部工作,第15章会再次讨论这个类,那时会演示如何添加应用程序专用的用户数据属性。

2. Creating the Database Context Class
2. 创建数据库上下文类

The next step is to create an Entity Framework database context that operates on the AppUser class. This will allow the Code First feature to create and manage the schema for the database and provide access to the data it stores. The context class is derived from IdentityDbContext<T>, where T is the user class (AppUser in the example). I created a folder called Infrastructure in the project and added to it a class file called AppIdentityDbContext.cs, the contents of which are shown in Listing 13-6.
下一个步骤是创建Entity Framework数据库的上下文,用于对AppUser类进行操作。这可以用Code First特性来创建和管理数据架构,并对数据库所存储的数据进行访问。这个上下文类派生于IdentityDbContext<T> ,其中的T是用户类(即此例中的AppUser)。我在项目中创建了一个文件夹,名称为Infrastructure,并在其中添加了一个类文件,名称为AppIdentityDbContext.cs,其内容如清单13-6所示。

Listing 13-6. The Contents of the AppIdentityDbContext.cs File
清单13-6. AppIdentityDbContext.cs文件的内容

using System.Data.Entity;
using Microsoft.AspNet.Identity.EntityFramework;
using Users.Models; 
namespace Users.Infrastructure { public class AppIdentityDbContext : IdentityDbContext<AppUser> {
public AppIdentityDbContext() : base("IdentityDb") { }
static AppIdentityDbContext() { Database.SetInitializer<AppIdentityDbContext>(new IdentityDbInit()); }
public static AppIdentityDbContext Create() { return new AppIdentityDbContext(); } }
public class IdentityDbInit : DropCreateDatabaseIfModelChanges<AppIdentityDbContext> {
protected override void Seed(AppIdentityDbContext context) { PerformInitialSetup(context); base.Seed(context); }
public void PerformInitialSetup(AppIdentityDbContext context) { // initial configuration will go here // 初始化配置将放在这儿 } } }

The constructor for the AppIdentityDbContext class calls its base with the name of the connection string that will be used to connect to the database, which is IdentityDb in this example. This is how I associate the connection string I defined in Listing 13-4 with ASP.NET Identity.
这个AppIdentityDbContext类的构造器调用了它的基类,其参数是连接字符串的名称,IdentityDb,用于与数据库连接。这是让清单13-4定义的连接字符串与ASP.NET Identity联结起来的方法。

The AppIdentityDbContext class also defines a static constructor, which uses the Database.SetInitializer method to specify a class that will seed the database when the schema is first created through the Entity Framework Code First feature. My seed class is called IdentityDbInit, and I have provided just enough of a class to create a placeholder so that I can return to seeding the database later by adding statements to the PerformInitialSetup method. I show you how to seed the database in Chapter 14.
这个AppIdentityDbContext类还定义了一个静态的构造器,它使用Database.SetInitializer方法指定了一个种植数据库的类(种植数据库的含义是往数据库中植入数据的意思,说穿了,就是用一些数据对数据库进行初始化——译者注),当通过Entity Framework的Code First特性第一次创建数据库架构时,会用到这个类。这个种植类叫做IdentityDbInit,而且我已经提供了一个类,创建了一个占位符,后面可以回过头来在PerformInitialSetup方法中添加语句,就可以种植数据库了。第14章将演示如何种植数据库。

Finally, the AppIdentityDbContext class defines a Create method. This is how instances of the class will be created when needed by the OWIN, using the class I describe in the “Creating the Start Class” section.
最后,AppIdentityDbContext类定义了一个Create方法。这是由OWIN在必要时创建类实例的办法,这个由OWIN使用的类将在“创建启动类”中进行描述。

Note Don't worry if the role of these classes doesn't make sense. If you are unfamiliar with the Entity Framework, then I suggest you treat it as something of a black box. Once the basic building blocks are in place—and you can copy the ones into your chapter to get things working—then you will rarely need to edit them.
注:如果对这些类的意义无法理解,不用担心。如果不熟悉Entity Framework,我建议你将它视为是某种黑箱事物。一旦基础构建块就绪——而且对本章的这些代码进行拷贝——那么就几乎不需要编辑它们了。

3. Creating the User Manager Class
3. 创建用户管理器类

One of the most important Identity classes is the user manager, which manages instances of the user class. The user manager class must be derived from UserManager<T> , where T is the user class. The UserManager<T> class isn't specific to the Entity Framework and provides more general features for creating and operating on user data. Table 13-3 shows the basic methods and properties defined by the UserManager<T> class for managing users. There are others, but rather than list them all here, I'll describe them in context when I describe the different ways in which user data can be managed.
最重要的Identity类之一是“User Manager(用户管理器)”,用来管理用户类实例。用户管理器类必须派生于UserManager<T> ,其中T是用户类。这个UserManager<T> 类并非是专用于Entity Framework的,而且它提供了很多通用特性,用以创建用户并对用户数据进行操作。表13-3列出了UserManager<T> 类为管理用户而定义的基本方法和操作。还有一些其他方法,这里并未全部列出来,我会在适当的情形下对它们进行描述,那时会介绍管理用户数据的不同方式。

Table 13-3. The Basic Methods and Properties Defined by the UserManager<T> Class
表13-3. UserManager<T>类所定义的基本方法和操作
Name
名称
Description
描述
ChangePasswordAsync(id, old, new) Changes the password for the specified user.
为指定用户修改口令
CreateAsync(user) Creates a new user without a password. See Chapter 15 for an example.
创建一个不带口令的新用户,参见第15章示例
CreateAsync(user, pass) Creates a new user with the specified password. See the “Creating Users” section.
创建一个带有指定口令的新用户,参见“创建用户”
DeleteAsync(user) Deletes the specified user. See the “Implementing the Delete Feature” section.
删除指定用户,参见“实现Delete特性”
FindAsync(user, pass) Finds the object that represents the user and authenticates their password. See Chapter 14 for details of authentication.
查找代表该用户的对象,并认证其口令,详见第14章的认证细节
FindByIdAsync(id) Finds the user object associated with the specified ID. See the “Implementing the Delete Feature” section.
查找与指定ID相关联的用户对象,参见“实现Delete特性”
FindByNameAsync(name) Finds the user object associated with the specified name. I use this method in the “Seeding the Database” section of Chapter 14.
查找与指定名称相关联的用户对象,第14章“种植数据库”时会用到这个方法
UpdateAsync(user) Pushes changes to a user object back into the database. See the “Implementing the Edit Feature” section.
将用户对象的修改送入数据库,参见“实现Edit特性”
Users Returns an enumeration of the users. See the “Enumerating User Accounts” section.
返回用户枚举,参见“枚举用户账号”

Tip Notice that the names of all of these methods end with Async. This is because ASP.NET Identity is implemented almost entirely using C# asynchronous programming features, which means that operations will be performed concurrently and not block other activities. You will see how this works once I start demonstrating how to create and manage user data. There are also synchronous extension methods for each Async method. I stick to the asynchronous methods for most examples, but the synchronous equivalents are useful if you need to perform multiple related operations in sequence. I have included an example of this in the “Seeding the Database” section of Chapter 14. The synchronous methods are also useful when you want to call Identity methods from within property getters or setters, which I do in Chapter 15.
提示:请注意方法名以Async结尾的那些方法。因为ASP.NET Identity几乎完全是用C#的异步编程特性实现的,这意味着会并发地执行各种操作,而不会阻塞其他活动。在我开始演示如何创建和管理用户数据时,你便会看到这种情况。对于每一个Async方法也有相应的同步扩展方法。对于大多数示例,我会坚持这种异步方法。但是,如果你需要按顺序执行一些相关的操作,同步方法是有用的。在第14章的“种植数据库”中就包含了一个这样的例子。当你希望从属性内部的getter或setter代码块中调用Identity方法时,同步方法也是有用的,在第15章中,我就是这么做的。

I added a class file called AppUserManager.cs to the Infrastructure folder and used it to define the user manager class, which I have called AppUserManager, as shown in Listing 13-7.
我在Infrastructure文件夹中添加了一个类文件,名称为AppUserManager.cs,用它定义了用户管理器类,名称为AppUserManager,如清单13-7所示。

Listing 13-7. The Contents of the AppUserManager.cs File
清单13-7. AppUserManager.cs文件的内容

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Users.Models; 
namespace Users.Infrastructure { public class AppUserManager : UserManager<AppUser> {
public AppUserManager(IUserStore<AppUser> store) : base(store) { }
public static AppUserManager Create( IdentityFactoryOptions<AppUserManager> options, IOwinContext context) { AppIdentityDbContext db = context.Get<AppIdentityDbContext>(); AppUserManager manager = new AppUserManager(new UserStore<AppUser>(db)); return manager; } } }

The static Create method will be called when Identity needs an instance of the AppUserManager, which will happen when I perform operations on user data—something that I will demonstrate once I have finished performing the setup.
在Identity需要一个AppUserManager的实例时,将会调用静态的Create方法,这种情况将在对用户数据执行操作时发生——也是在完成设置之后要演示的事情。

To create an instance of the AppUserManager class, I need an instance of UserStore<AppUser> . The UserStore<T> class is the Entity Framework implementation of the IUserStore<T> interface, which provides the storage-specific implementation of the methods defined by the UserManager class. To create the UserStore<AppUser> , I need an instance of the AppIdentityDbContext class, which I get through OWIN as follows:
为了创建AppUserManager类的实例,我需要一个UserStore<AppUser> 实例。这个UserStore<T> 类是IUserStore<T> 接口的Entity Framework实现,它提供了UserManager类所定义的存储方法的实现。为了创建UserStore<AppUser> ,又需要AppIdentityDbContext类的一个实例,这是通过OWIN按如下办法得到的:

...
AppIdentityDbContext db = context.Get<AppIdentityDbContext>();
...

The IOwinContext implementation passed as an argument to the Create method defines a generically typed Get method that returns instances of objects that have been registered in the OWIN start class, which I describe in the following section.
被作为参数传递给Create方法的IOwinContext实现定义了一个泛型的Get方法,它会返回已经在OWIN启动类中注册的对象实例,启动类在以下小节描述。

4. Creating the Start Class
4. 创建启动类

The final piece I need to get ASP.NET Identity up and running is a start class. In Listing 13-4, I defined an application setting that specified a configuration class for OWIN, like this:
为了使ASP.NET Identity就绪并能运行,要做的最后一个片段是Start Class(启动类)。在清单13-4中,我定义了一个应用程序设置,它为OWIN指定配置类,如下所示:

...
<add key="owin:AppStartup" value="Users.IdentityConfig" />
...

OWIN emerged independently of ASP.NET and has its own conventions. One of them is that there is a class that is instantiated to load and configure middleware and perform any other configuration work that is required. By default, this class is called Start, and it is defined in the global namespace. This class contains a method called Configuration, which is called by the OWIN infrastructure and passed an implementation of the Owin.IAppBuilder interface, which supports setting up the middleware that an application requires. The Start class is usually defined as a partial class, with its other class files dedicated to each kind of middleware that is being used.
OWIN是独立出现在ASP.NET中的,并且有它自己的约定。其中之一就是,为了加载和配置中间件,并执行所需的其他配置工作,需要有一个被实例化的类。默认情况下,这个类叫做Start,而且是在全局命名空间中定义的。这个类含有一个名称为Configuration的方法,该方法由OWIN基础架构进行调用,并为该方法传递一个Owin.IAppBuilder接口的实现,由它支持应用程序所需中间件的设置。Start类通常被定义成分部类,还有一些其他的类文件,它们分别专用于要用的每一种中间件。

I freely ignore this convention, given that the only OWIN middleware that I use in MVC framework applications is Identity. I prefer to use the application setting in the Web.config file to define a single class in the top-level namespace of the application. To this end, I added a class file called IdentityConfig.cs to the App_Start folder and used it to define the class shown in Listing 13-8, which is the class that I specified in the Web.config folder.
我随意地忽略了这一约定,因为我在MVC框架应用程序中使用的唯一OWIN中间件就是Identity。为了在应用程序的顶级命名空间定义一个类,我喜欢在Web.config文件中使用应用程序设置。于是我在App_Start文件夹中添加了一个类文件,名称为IdentityConfig.cs,并用它定义了如清单13-8所示的类,这是我在Web.config文件中指定的一个类。

Listing 13-8. The Contents of the IdentityConfig.cs File
清单13-8. IdentityConfig.cs文件的内容

using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Owin;
using Users.Infrastructure; 
namespace Users { public class IdentityConfig { public void Configuration(IAppBuilder app) {
app.CreatePerOwinContext<AppIdentityDbContext>(AppIdentityDbContext.Create); app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);
app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Account/Login"), }); } } }

The IAppBuilder interface is supplemented by a number of extension methods defined in classes in the Owin namespace. The CreatePerOwinContext method creates a new instance of the AppUserManager and AppIdentityDbContext classes for each request. This ensures that each request has clean access to the ASP.NET Identity data and that I don't have to worry about synchronization or poorly cached database data.
IAppBuilder接口是由一些扩展方法提供的,这些扩展方法的定义在Owin命名空间的一些类中。CreatePerOwinContext用于创建AppUserManager的新实例,AppIdentityDbContext类用于每一个请求。这样保证每一个请求对ASP.NET Identity数据有清晰的访问,我不必为同步时的情况或匮乏的数据缓存而操心。

The UseCookieAuthentication method tells ASP.NET Identity how to use a cookie to identity authenticated users, where the options are specified through the CookieAuthenticationOptions class. The important part here is the LoginPath property, which specifies a URL that clients should be redirected to when they request content without authentication. I have specified /Account/Login, and I will create the controller that handles these redirections in Chapter 14.
UseCookieAuthentication方法告诉ASP.NET Identity如何用Cookie去标识已认证的用户,以及通过CookieAuthenticationOptions类指定的选项在哪儿。这里重要的部分是LoginPath属性,它指定了一个URL,这是未认证客户端请求内容时要重定向的地址。我在其中指定了/Account/Login,将在第14章创建这个控制器来处理这些重定向。

13.3 Using ASP.NET Identity
13.3 使用ASP.NET Identity

Now that the basic setup is out of the way, I can start to use ASP.NET Identity to add support for managing users to the example application. In the sections that follow, I will demonstrate how the Identity API can be used to create administration tools that allow for centralized management of users. Table 13-4 puts ASP.NET Identity into context.
现在,已经完成了基本设置,可以开始使用ASP.NET Identity在示例应用程序中添加对用户管理的支持了。在以下几小节中,我将演示如何将Identity API用于创建管理工具,这样能够集中化地管理用户。表13-4说明了ASP.NET Identity的情形。

Table 13-4. Putting Content Caching by Attribute in Context
表13-4. ASP.NET Identity的情形(这个标题作者写错了——译者注)
Question
问题
Answer
回答
What is it?
什么是ASP.NET Identity?
ASP.NET Identity is the API used to manage user data and perform authentication and authorization.
ASP.NET Identity是用来管理用户数据并执行认证和授权的API
Why should I care?
为何要关注它?
Most applications require users to create accounts and provide credentials to access content and features. ASP.NET Identity provides the facilities for performing these operations.
大多数应用程序都需要用户创建账号,并提供凭据去访问内容和功能。ASP.NET Identity提供了执行这些操作的工具
How is it used by the MVC framework?
如何在MVC框架中使用它?
ASP.NET Identity isn't used directly by the MVC framework but integrates through the standard MVC authorization features.
ASP.NET Identity不是由MVC框架直接使用的,但它集成了标准的MVC授权特性

13.3.1 Enumerating User Accounts
13.3.1 枚举用户账号

Centralized user administration tools are useful in just about all applications, even those that allow users to create and manage their own accounts. There will always be some customers who require bulk account creation, for example, and support issues that require inspection and adjustment of user data. From the perspective of this chapter, administration tools are useful because they consolidate a lot of basic user management functions into a small number of classes, making them useful examples to demonstrate the fundamental features of ASP.NET Identity.
集中化的用户管理工具在几乎所有应用程序中都是有用的,即使是那些允许用户创建并管理自己账号的情况也是如此。例如,总会有这样一些客户,他们需要大量创建账号,并支持需要检查和调整用户数据的问题。根据本章的观点,管理工具是有用的,因为它们将许多基本的用户管理功能整合到了少量的几个类之中,这对于演示ASP.NET Identity的基本特性是很有用的。

I started by adding a controller called Admin to the project, which is shown in Listing 13-9, and which I will use to define my user administration functionality.
首先,在项目中添加一个控制器,名称为Admin,如清单13-9所示,我将用它定义我的用户管理功能。

Listing 13-9. The Contents of the AdminController.cs File
清单13-9. AdminController.cs文件的内容

using System.Web;
using System.Web.Mvc;
using Microsoft.AspNet.Identity.Owin;
using Users.Infrastructure; 
namespace Users.Controllers { public class AdminController : Controller {
public ActionResult Index() { return View(UserManager.Users); }
private AppUserManager UserManager { get { return HttpContext.GetOwinContext().GetUserManager<AppUserManager>(); } } } }

The Index action method enumerates the users managed by the Identity system; of course, there aren't any users at the moment, but there will be soon. The important part of this listing is the way that I obtain an instance of the AppUserManager class, through which I manage user information. I will be using the AppUserManager class repeatedly as I implement the different administration functions, and I defined the UserManager property in the Admin controller to simplify my code.
动作方法枚举由Identity系统管理的用户,当然,此刻还没有任何用户,但马上就会有了。该清单重要的部分是我获得类实例的方式,通过它,我可以管理用户信息。在我实现不同的管理功能时,我会反复使用AppUserManager类,而且,我在Admin控制器中定义了UserManager属性,以便简化代码。

The Microsoft.Owin.Host.SystemWeb assembly adds extension methods for the HttpContext class, one of which is GetOwinContext. This provides a per-request context object into the OWIN API through an IOwinContext object. The IOwinContext isn't that interesting in an MVC framework application, but there is another extension method called GetUserManager<T> that is used to get instances of the user manager class.
Microsoft.Owin.Host.SystemWeb程序集为HttpContext类添加了一些扩展方法,其中之一便是GetOwinContext。它通过一个IOwinContext对象,将基于每请求的上下文对象提供给WOIN API。MVC框架应用程序对IOwinContext不感兴趣,但是有另外一个扩展方法,叫做GetUserManager<T> ,可以用来得到用户管理器类的实例。

Tip As you may have gathered, there are lots of extension methods in ASP.NET Identity; overall, the API is something of a muddle as it tries to mix OWIN, abstract Identity functionality, and the concrete Entity Framework storage implementation.
提示:正如你所推断的一样,ASP.NET Identity中有许多扩展方法,总体而言,这个API是一种混合体,它试图将WOIN、抽象Identity功能以及具体的Entity Framework存储实现混合在一起。

I called the GetUserManager with a generic type parameter to specify the AppUserManager class that I created earlier in the chapter, like this:
我用一个泛型参数调用了GetUserManager,用以指定本章前面创建的AppUserManager类,如下所示:

...
return HttpContext.GetOwinContext().GetUserManager<AppUserManager>();
...

Once I have an instance of the AppUserManager class, I can start to query the data store. The AppUserManager.Users property returns an enumeration of user objects—instances of the AppUser class in my application—which can be queried and manipulated using LINQ.
一旦有了AppUserManager类的实例,便可以开始查询数据存储了。AppUserManager.Users属性返回了一个用户对象的枚举——应用程序中AppUser类的实例——于是可以用LINQ对这个枚举进行查询和维护。

In the Index action method, I pass the value of the Users property to the View method so that I can list details of the users in the view. Listing 13-10 shows the contents of the Views/Admin/Index.cshtml file that I created by right-clicking the Index action method and selecting Add View from the pop-up menu.
Index动作方法中,给View方法传递了Users属性的值,以便能够在视图列出用户的细节。清单13-10显示了Views/Admin/Index.cshtml文件的内容,这是通过右击Index动作方法,并从弹出菜单选择“Add View(添加视图)”而创建的。

Listing 13-10. The Contents of the Index.cshtml File in the /Views/Admin Folder
清单13-10. /Views/Admin文件夹中Index.cshtml文件的内容

@using Users.Models
@model IEnumerable<AppUser>
@{
    ViewBag.Title = "Index";
}
<div class="panel panel-primary"> <div class="panel-heading"> User Accounts </div> <table class="table table-striped">
<tr><th>ID</th><th>Name</th><th>Email</th></tr> @if (Model.Count() == 0) { <tr><td colspan="3" class="text-center">No User Accounts</td></tr> } else { foreach (AppUser user in Model) { <tr> <td>@user.Id</td> <td>@user.UserName</td> <td>@user.Email</td> </tr> } } </table> </div> @Html.ActionLink("Create", "Create", null, new { @class = "btn btn-primary" })

This view contains a table that has rows for each user, with columns for the unique ID, username, and e-mail address. If there are no users in the database, then a message is displayed, as shown in Figure 13-4.
这个视图含有一个表格,每个用户为一行,带有唯一ID、用户名以及E-mail地址的表格列。如果数据库中没有用户,那么会显示一条消息(图中的“No User Accounts”消息——译者注),如图13-4所示。

图13-4

Figure 13-4. Display the (empty) list of users
图13-4. 显示用户列表(空)

I included a Create link in the view (which I styled as a button using Bootstrap) that targets the Create action on the Admin controller. I'll implement this action shortly to support adding users.
在视图中有一个Create的链接(其样式用Bootstrap形成了一个按钮),它的目标是Admin控制器中的Create动作。为了支持添加用户,我会很快实现这个动作。

RESETTING THE DATABASE
重置数据库

When you start the application and navigate to the /Admin/Index URL, it will take a few moments before the contents rendered from the view are displayed. This is because the Entity Framework has connected to the database and determined that there is no schema defined. The Code First feature uses the classes I defined earlier in the chapter (and some which are contained in the Identity assemblies) to create the schema so that it is ready to be queried and to store data.
当启动应用程序并导航到/Admin/Index URL时,在视图渲染的内容被显示出来之前,会花一些时间。这是因为Entity Framework已经链接到数据库,并发现此时尚未定义数据库架构。Code First特性使用本章前面定义的类(以及包含在Identity程序集中的类)创建该架构,以便做好查询和存储数据的准备。

You can see the effect by opening the Visual Studio SQL Server Object Explorer window and expanding entry for the IdentityDB database schema, which will include tables with names such as AspNetUsers and AspNetRoles.
通过打开Visual Studio的“SQL Server Object Explorer(SQL Server对象资源管理器)”窗口,并展开IdentityDB数据库架构,便可以看到其中包含了一些数据表,如AspNetUsersAspNetRoles等等 。

To delete the database, right-click the IdentityDb item and select Delete from the pop-up menu. Check both of the options in the Delete Database dialog and click the OK button to delete the database.
要删除该数据库,右击IdentityDb条目,并从弹出菜单选择“Delete(删除)”。选中“Delete Database(删除数据库)”对话框中的复选框,并点击OK,以删除该数据库。

Right-click the Databases item, select Add New Database (as shown in Figure 13-3), and enter IdentityDb in the Database Name field. Click OK to create the empty database. The next time you start the application and navigate to the Admin/Index URL, the Entity Framework will detect that there is no schema and re-create it.
右击“Databases(数据库)”条目,选择“Add New Database(添加新数据库)”(如图13-3所示),并在“Database Name(数据库名)”字段中输入IdentityDb,点击OK,便可以创建空数据库。下一次启动应用程序,并导航到Admin/Index URL时,Entity Framework又将侦测到没有数据库架构,又会重新创建此数据库。

13.3.2 Creating Users
13.3.2 创建用户

I am going to use MVC framework model validation for the input my application receives, and the easiest way to do this is to create simple view models for each of the operations that my controller supports. I added a class file called UserViewModels.cs to the Models folder and used it to define the class shown in Listing 13-11. I'll add further classes to this file as I define models for additional features.
对于应用程序接收的输入,我打算使用MVC架构的模型验证,最容易的做法是为控制器所支持的每一种操作创建一个简单的视图模型。我在Models文件夹中添加了一个类文件,名称为serViewModels.cs,并用它定义了如清单13-11所示的类。随着我为其他特性定义模型,会进一步地在这个文件中添加一些类。

Listing 13-11. The Contents of the UserViewModels.cs File
清单13-11. UserViewModels.cs文件的内容

using System.ComponentModel.DataAnnotations; 
namespace Users.Models {
public class CreateModel { [Required] public string Name { get; set; } [Required] public string Email { get; set; } [Required] public string Password { get; set; } } }

The initial model I have defined is called CreateModel, and it defines the basic properties that I require to create a user account: a username, an e-mail address, and a password. I have used the Required attribute from the System.ComponentModel.DataAnnotations namespace to denote that values are required for all three properties defined in the model.
我所定义的第一个模型叫做CreateModel,它定义了创建用户账号所需要的基本属性:用户名、E-mail地址以及口令。这个模型中所定义的所有属性都使用了System.ComponentModel.DataAnnotations命名空间中的Required注释属性,用以说明这些值是必须的。

I added a pair of Create action methods to the Admin controller, which are targeted by the link in the Index view from the previous section and which uses the standard controller pattern to present a view to the user for a GET request and process form data for a POST request. You can see the new action methods in Listing 13-12.
我在Admin控制器中添加了一对Create动作方法,它们是上一小节Index视图中的那个链接所指向的目标,而且,对于GET请求以及处理表单数据的POST请求,使用的是标准的控制器模式将视图表现给用户。可以从清单13-12中看到这两个新的动作方法。

Listing 13-12. Defining the Create Action Methods in the AdminController.cs File
清单13-12. 在AdminController.cs文件中定义Create动作方法

using System.Web;
using System.Web.Mvc;
using Microsoft.AspNet.Identity.Owin;
using Users.Infrastructure;
using Users.Models;
using Microsoft.AspNet.Identity;
using System.Threading.Tasks; 
namespace Users.Controllers { public class AdminController : Controller {
public ActionResult Index() { return View(UserManager.Users); }
public ActionResult Create() { return View(); }
[HttpPost] public async Task<ActionResult> Create(CreateModel model) { if (ModelState.IsValid) { AppUser user = new AppUser {UserName = model.Name, Email = model.Email}; IdentityResult result = await UserManager.CreateAsync(user, model.Password); if (result.Succeeded) { return RedirectToAction("Index"); } else { AddErrorsFromResult(result); } } return View(model); }
private void AddErrorsFromResult(IdentityResult result) { foreach (string error in result.Errors) { ModelState.AddModelError("", error); } }

private AppUserManager UserManager { get { return HttpContext.GetOwinContext().GetUserManager<AppUserManager>(); } } } }

The important part of this listing is the Create method that takes a CreateModel argument and that will be invoked when the administrator submits their form data. I use the ModelState.IsValid property to check that the data I am receiving contains the values I require, and if it does, I create a new instance of the AppUser class and pass it to the UserManager.CreateAsync method, like this:
该清单重要的部分是以CreateModel为参数并在管理员递交其表单数据时调用的那个Create动作方法(POST版的Create动作方法——译者注)。我用ModelState.IsValid属性检查所接收到的数据是否包含了我所需要的数据,如果是,便创建一个AppUser类的新实例,并将它传递给UserManager.CreateAsync方法,如下所示:

...
AppUser user = new AppUser {UserName = model.Name, Email = model.Email};
IdentityResult result = await UserManager.CreateAsync(user, model.Password); 
...

The result from the CreateAsync method is an implementation of the IdentityResult interface, which describes the outcome of the operation through the properties listed in Table 13-5.
CreateAsync方法的结果是一个IdentityResult接口的实现,它通过表13-5中的属性描述操作的输出。

Table 13-5. The Properties Defined by the IdentityResult Interface
表13-5. IdentityResult接口所定义的属性
Name
名称
Description
描述
Errors Returns a string enumeration that lists the errors encountered while attempting the operation
返回一个字符串枚举,其中列出了尝试操作期间所遇到的错误
Succeeded Returns true if the operation succeeded
在操作成功时返回true

USING THE ASP.NET IDENTITY ASYNCHRONOUS METHODS
使用ASP.NET Identity异步方法

You will notice that all of the operations for manipulating user data, such as the UserManager.CreateAsync method I used in Listing 13-12, are available as asynchronous methods. Such methods are easily consumed with the async and await keywords. Using asynchronous Identity methods allows your action methods to be executed asynchronously, which can improve the overall throughput of your application.
你会注意到,维护用户数据的所有操作,如清单13-12中所用到的UserManager.CreateAsync方法,都可以作为异步方法来使用。这种方法很容易与asyncawait关键字一起使用。使用异步的Identity方法让你的动作方法能够异步执行,这可以从整体上改善应用程序。

However, you can also use synchronous extension methods provided by the Identity API. All of the commonly used asynchronous methods have a synchronous wrapper so that the functionality of the UserManager.CreateAsync method can be called through the synchronous UserManager.Create method. I use the asynchronous methods for preference, and I recommend you follow the same approach in your projects. The synchronous methods can be useful for creating simpler code when you need to perform multiple dependent operations, so I used them in the “Seeding the Database” section of Chapter 14 as a demonstration.
然而,你也可以使用那些由Identity API提供的同步扩展方法。所有常用的异步方法都有一个同步的封装程序,因此UserManager.CreateAsync方法的功能可以通过同步的UserManager.Create方法进行调用。我更喜欢使用异步方法,而且我建议在你的项目中遵循同样的方式。当需要执行多个有依赖的操作时,为了形成简单的代码,同步方法可能是有用的,因此,作为一个演示,我在第14章的“种植数据库”小节中使用了它们。

I inspect the Succeeded property in the Create action method to determine whether I have been able to create a new user record in the database. If the Succeeded property is true, then I redirect the browser to the Index action so that list of users is displayed:
Create动作方法中,我检测了Succeeded属性,以确定是否能够在数据库创建一条新的用户记录。如果Succeeded属性为true,那么便将浏览器重定向到Index动作,以便显示用户列表:

...
if (result.Succeeded) {
    return RedirectToAction("Index");
} else {
    AddErrorsFromResult(result);
}
...

If the Succeeded property is false, then I call the AddErrorsFromResult method, which enumerates the messages from the Errors property and adds them to the set of model state errors, taking advantage of the MVC framework model validation feature. I defined the AddErrorsFromResult method because I will have to process errors from other operations as I build the functionality of my administration controller. The last step is to create the view that will allow the administrator to create new accounts. Listing 13-13 shows the contents of the Views/Admin/Create.cshtml file.
如果Succeeded属性为false,那么便调用AddErrorsFromResult方法,该方法枚举了Errors属性中的消息,并将它们添加到模型状态的错误消息集合中,此过程利用了MVC框架的模型验证特性。我定义AddErrorsFromResult方法,是因为随着进一步地构建这个管理控制器的功能,还必须处理来自其他操作的错误消息。最后一步是创建视图,让管理员创建新账号。清单13-13显示了文件的Views/Admin/Create.cshtml内容。

Listing 13-13. The Contents of the Create.cshtml File
清单13-13. Create.cshtml文件的内容

@model Users.Models.CreateModel
@{ ViewBag.Title = "Create User";}
<h2>Create User</h2> @Html.ValidationSummary(false) @using (Html.BeginForm()) { <div class="form-group"> <label>Name</label> @Html.TextBoxFor(x => x.Name, new { @class = "form-control"}) </div> <div class="form-group"> <label>Email</label> @Html.TextBoxFor(x => x.Email, new { @class = "form-control" }) </div> <div class="form-group"> <label>Password</label> @Html.PasswordFor(x => x.Password, new { @class = "form-control" }) </div> <button type="submit" class="btn btn-primary">Create</button> @Html.ActionLink("Cancel", "Index", null, new { @class = "btn btn-default"}) }

There is nothing special about this view—it is a simple form that gathers values that the MVC framework will bind to the properties of the model class that is passed to the Create action method.
该视图没有什么特别的地方——只是一个简单的表单,用来收集一些值,MVC框架会将它们绑定到模型类的属性上,然后传递给Create动作方法。

1. Testing the Create Functionality
1. 测试Create功能

To test the ability to create a new user account, start the application, navigate to the /Admin/Index URL, and click the Create button. Fill in the form with the values shown in Table 13-6.
为了测试创建新用户账号的能力,启动应用程序,导航到/Admin/Index网址,并点击Create按钮。用一些值填充表单,如表13-6所示。

Table 13-6. The Values for Creating an Example User
表13-6. 创建示例用户的值
Name
名称
Value
Name
用户名
Joe
Email
E-mail地址
joe@example.com
Password
口令
Secret

Tip Although not widely known by developers, there are domains that are reserved for testing, including example.com. You can see a complete list at https://tools.ietf.org/html/rfc2606.
提示:虽然不是很多开发人员都知道,有一些域名是保留用于测试的,包括example.com。可以在https://tools.ietf.org/html/rfc2606网站看到完整的列表。

Once you have entered the values, click the Create button. ASP.NET Identity will create the user account, which will be displayed when your browser is redirected to the Index action method, as shown in Figure 13-5. You will see a different ID value because IDs are randomly generated for each user account.
一旦输入了这些值,点击“Create”按钮。ASP.NET Identity将创建此用户账号,当浏览器被重定向到Index动作方法,会显示出该用户的信息,如图13-5所示。从图中可以看到不同的ID值,这是因为对于每个用户账号,ID是随机生成的。

图13-5

Figure 13-5. The effect of adding a new user account
图13-5. 添加新用户账号的效果

Click the Create button again and enter the same details into the form, using the values in Table 13-6. This time when you submit the form, you will see an error reported through the model validation summary, as shown in Figure 13-6.
再次点击“Create”按钮,并在表章中输入同样细节,即,使用表13-6中的值。当这一次递交表单时,会看到一条通过模型验证摘要报告的错误,如图13-6所示。

图13-6

Figure 13-6. An error trying to create a new user
图13-6. 试图创建新用户时的错误

13.3.3 Validating Passwords
13.3.3 验证口令

One of the most common requirements, especially for corporate applications, is to enforce a password policy. ASP.NET Identity provides the PasswordValidator class, which can be used to configure a password policy using the properties described in Table 13-7.
一个最常用的需求,特别是对于公司的应用程序,是强制口令策略。ASP.NET Identity提供了一个PasswordValidator类,可以用表13-7所描述的属性来配置口令策略。

Table 13-7. The Properties Defined by the PasswordValidator Class
表13-7. PasswordValidator类定义的属性
Name
名称
Description
描述
RequiredLength Specifies the minimum length of a valid passwords.
指定合法口令的最小长度
RequireNonLetterOrDigit When set to true, valid passwords must contain a character that is neither a letter nor a digit.
当设置为true时,合法口令必须含有非字母和数字的字符
RequireDigit When set to true, valid passwords must contain a digit.
当设置为true时,合法口令必须含有数字
RequireLowercase When set to true, valid passwords must contain a lowercase character.
当设置为true时,合法口令必须含有小写字母
RequireUppercase When set to true, valid passwords must contain an uppercase character.
当设置为true时,合法口令必须含有大写字母

A password policy is defined by creating an instance of the PasswordValidator class, setting the property values, and using the object as the value for the PasswordValidator property in the Create method that OWIN uses to instantiate the AppUserManager class, as shown in Listing 13-14.
定义口令策略的办法是,创建一个PasswordValidator类实例、设置其属性的值,并在OWIN用来实例化AppUserManager类的Create方法中将该对象作为PasswordValidator属性的值,如清单13-14所示。

Listing 13-14. Setting a Password Policy in the AppUserManager.cs File
清单13-14. 在AppUserManager.cs文件中设置口令策略

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Users.Models; 
namespace Users.Infrastructure { public class AppUserManager : UserManager<AppUser> {
public AppUserManager(IUserStore<AppUser> store) : base(store) { } public static AppUserManager Create(IdentityFactoryOptions<AppUserManager> options, IOwinContext context) {
AppIdentityDbContext db = context.Get<AppIdentityDbContext>(); AppUserManager manager = new AppUserManager( new UserStore<AppUser>(db));
manager.PasswordValidator = new PasswordValidator { RequiredLength = 6, RequireNonLetterOrDigit = false, RequireDigit = false, RequireLowercase = true, RequireUppercase = true };

return manager; } } }

I used the PasswordValidator class to specify a policy that requires at least six characters and a mix of uppercase and lowercase characters. You can see how the policy is applied by starting the application, navigating to the /Admin/Index URL, clicking the Create button, and trying to create an account that has the password secret. The password doesn't meet the new password policy, and an error is added to the model state, as shown in Figure 13-7.
我用PasswordValidator类指定了一个策略,它要求至少6个字符,并混用大小字符。通过启动应用程序,便可以看到该口令策略的运用情况,导航到/Admin/Index网址,点击Create按钮,并尝试创建一个有口令加密的账号。若口令不满足这一新策略,便会在模型状态中添加一条错误消息,如图13-7所示。

图13-7

Figure 13-7. Reporting an error when validating a password
图13-7. 验证口令时报告的错误

Implementing a Custom Password Validator
实现自定义口令验证器

The built-in password validation is sufficient for most applications, but you may need to implement a custom policy, especially if you are implementing a corporate line-of-business application where complex password policies are common. Extending the built-in functionality is done by deriving a new class from PasswordValidatator and overriding the ValidateAsync method. As a demonstration, I added a class file called CustomPasswordValidator.cs in the Infrastructure folder and used it to define the class shown in Listing 13-15.
内建的口令验证对于大多数应用程序足够了,但你可能需要实现自定义的策略,尤其是在你实现一个公司的在线业务应用程序时,往往需要复杂的口令策略。这种对内建功能进行扩展的做法是,从PasswordValidatator派生一个新类,并重写ValidateAsync方法。作为一个演示,我在Infrastructure文件夹中添加了一个类文件,名称为CustomPasswordValidator.cs,如清单13-15所示。

Listing 13-15. The Contents of the CustomPasswordValidator.cs File
清单13-15. CustomPasswordValidator.cs文件的内容

using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity; 
namespace Users.Infrastructure { public class CustomPasswordValidator : PasswordValidator {
public override async Task<IdentityResult> ValidateAsync(string pass) { IdentityResult result = await base.ValidateAsync(pass); if (pass.Contains("12345")) { var errors = result.Errors.ToList(); errors.Add("Passwords cannot contain numeric sequences"); result = new IdentityResult(errors); } return result; } } }

I have overridden the ValidateAsync method and call the base implementation so I can benefit from the built-in validation checks. The ValidateAsync method is passed the candidate password, and I perform my own check to ensure that the password does not contain the sequence 12345. The properties of the IdentityResult class are read-only, which means that if I want to report a validation error, I have to create a new instance, concatenate my error with any errors from the base implementation, and pass the combined list as the constructor argument. I used LINQ to concatenate the base errors with my custom one.
这里重写了ValidateAsync方法,并调用了基实现,因此能够受益于内建的验证检查。给ValidateAsync方法传递了申请的口令,然后执行了自己的检查,确保口令中不含数据序列12345。IdentityResult类的属性是只读的,这意味着,如果想报告验证错误,则必须创建一个新实例,把这些错误与基实现的错误联系在一起,并将这种组合而成的列表作为构造器参数进行传递。为了将基实现的错误与自定义错误联系起来,我使用了LINQ。

Listing 13-16 shows the application of my custom password validator in the AppUserManager class.
清单13-6显示了在AppUserManager类中使用自定义口令验证器的应用程序。

Listing 13-16. Applying a Custom Password Validator in the AppUserManager.cs File
清单13-16. 在AppUserManager.cs文件中运用自定义口令验证器

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Users.Models; 
namespace Users.Infrastructure { public class AppUserManager : UserManager<AppUser> {
public AppUserManager(IUserStore<AppUser> store) : base(store) { }
public static AppUserManager Create(IdentityFactoryOptions<AppUserManager> options, IOwinContext context) {
AppIdentityDbContext db = context.Get<AppIdentityDbContext>(); AppUserManager manager = new AppUserManager( new UserStore<AppUser>(db));
manager.PasswordValidator = new CustomPasswordValidator { RequiredLength = 6, RequireNonLetterOrDigit = false, RequireDigit = false, RequireLowercase = true, RequireUppercase = true };
return manager; } } }

To test the custom password validation, try to create a new user account with the password secret12345. This will break two of the validation rules—one from the built-in validator and one from my custom implementation. Error messages for both problems are added to the model state and displayed when the Create button is clicked, as shown in Figure 13-8.
为了测试自定义口令验证器,尝试创建一个以口令密码为12345的新用户账号。这会打破两条规则——一条来自于内建验证器,一条来自于自定义实现。两个错误的错误消息被添加到了模型状态,并在点击Create按钮时被显示出来,如图13-8所示。

图13-8

Figure 13-8. The effect of a custom password validation policy
图13-8. 自定义口令验证策略的效果

13.3.4 Validating User Details
13.3.4 验证用户细节

More general validation can be performed by creating an instance of the UserValidator class and using the properties it defines to restrict other user property values. Table 13-8 describes the UserValidator properties.
还可以执行更一般的验证,办法是创建UserValidator类的实例,并使用它所定义的属性,以限制用户其他属性的值。表13-8描述了UserValidator的属性。

Table 13-8. The Properties Defined by the UserValidator Class
表13-8. UserValidator类所定义的属性
Name
名称
Description
描述
AllowOnlyAlphanumericUserNames When true, usernames can contain only alphanumeric characters.
当为true时,用户名只能含有字母数字字符
RequireUniqueEmail When true, e-mail addresses must be unique.
当为true时,邮件地址必须唯一

Performing validation on user details is done by creating an instance of the UserValidator class and assigning it to the UserValidator property of the user manager class within the Create method that OWIN uses to create instances. Listing 13-17 shows an example of using the built-in validator class.
对用户细节执行验证的做法是创建UserValidator类实例,并在OWIN用来创建实例的Create方法中,将它赋给用户管理器类的UserValidator属性。清单13-17演示了使用内建验证器类的一个示例。

Listing 13-17. Using the Built-in user Validator Class in the AppUserManager.cs File
清单13-17. 在AppUserManager.cs文件中使用内建的验证器类

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Users.Models; 
namespace Users.Infrastructure { public class AppUserManager : UserManager<AppUser> {
public AppUserManager(IUserStore<AppUser> store) : base(store) { }
public static AppUserManager Create(IdentityFactoryOptions<AppUserManager> options, IOwinContext context) {
AppIdentityDbContext db = context.Get<AppIdentityDbContext>(); AppUserManager manager = new AppUserManager( new UserStore<AppUser>(db));
manager.PasswordValidator = new CustomPasswordValidator { RequiredLength = 6, RequireNonLetterOrDigit = false, RequireDigit = false, RequireLowercase = true, RequireUppercase = true };
manager.UserValidator = new UserValidator<AppUser>(manager) { AllowOnlyAlphanumericUserNames = true, RequireUniqueEmail = true };
return manager; } } }

The UserValidator class takes a generic type parameter that specifies the type of the user class, which is AppUser in this case. Its constructor argument is the user manager class, which is an instance of the user manager class (which is AppUserManager for my application).
UserValidator类有一个泛型的类型参数,它指定了用户类的类型,即本示例中的AppUser。它的构造器参数是用户管理器类,这是用户管理器类(此应用程序中的AppUserManager)的一个实例。

The built-in validation support is rather basic, but you can create a custom validation policy by creating a class that is derived from UserValidator. As a demonstration, I added a class file called CustomUserValidator.cs to the Infrastructure folder and used it to create the class shown in Listing 13-18.
内建的验证支持是相当基本的,但通过创建UserValidator的派生类,可以创建自定义验证策略。作为一个演示,我在Infrastructure文件夹中添加了一个名称为CustomUserValidator.cs的类文件,并用它创建了如清单13-19所示的类。

Listing 13-18. The Contents of the CustomUserValidator.cs File
清单13-18. CustomUserValidator.cs文件的内容

using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Users.Models; 
namespace Users.Infrastructure { public class CustomUserValidator : UserValidator<AppUser> {
public CustomUserValidator(AppUserManager mgr) : base(mgr) { }
public override async Task<IdentityResult> ValidateAsync(AppUser user) { IdentityResult result = await base.ValidateAsync(user); if (!user.Email.ToLower().EndsWith("@example.com")) { var errors = result.Errors.ToList(); errors.Add("Only example.com email addresses are allowed"); result = new IdentityResult(errors); } return result; } } }

The constructor of the derived class must take an instance of the user manager class and call the base implementation so that the built-in validation checks can be performed. Custom validation is implemented by overriding the ValidateAsync method, which takes an instance of the user class and returns an IdentityResult object. My custom policy restricts users to e-mail addresses in the example.com domain and performs the same LINQ manipulation I used for password validation to concatenate my error message with those produced by the base class. Listing 13-19 shows how I applied my custom validation class in the Create method of the AppUserManager class, replacing the default implementation.
这个派生类的构造器必须以用户管理器类实例为参数,并调用基实现,才能够执行内建的验证检查。自定义验证是通过重写ValidateAsync方法而实现的,该方法以用户类实例为参数,并返回一个IdentityResult对象。上述自定义验证策略将用户的E-mail地址限制在example.com主域内,并执行了与口令验证同样的LINQ操作,将这里的错误消息与基类产生的错误消息关联在一起。清单13-19演示了如何在AppUserManager类的Create方法中运用自定义验证类,用以替换默认的实现。

Listing 13-19. Using a Custom User Validation Class in the AppUserManager.cs File
清单13-19. 在AppUserManager.cs文件中使用自定义用户验证类

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Users.Models; 
namespace Users.Infrastructure { public class AppUserManager : UserManager<AppUser> {
public AppUserManager(IUserStore<AppUser> store) : base(store) { }
public static AppUserManager Create(IdentityFactoryOptions<AppUserManager> options, IOwinContext context) {
AppIdentityDbContext db = context.Get<AppIdentityDbContext>(); AppUserManager manager = new AppUserManager( new UserStore<AppUser>(db));
manager.PasswordValidator = new CustomPasswordValidator { RequiredLength = 6, RequireNonLetterOrDigit = false, RequireDigit = false, RequireLowercase = true, RequireUppercase = true };
manager.UserValidator = new CustomUserValidator(manager) { AllowOnlyAlphanumericUserNames = true, RequireUniqueEmail = true };

return manager; } } }

You can see the result if you try to create an account with an e-mail address such as bob@otherdomain.com, as shown in Figure 13-9.
如果试图创建一个E-mail地址为bob@otherdomain.com的账号,便会看到如图13-9所示的结果。

图13-9

Figure 13-9. An error message shown by a custom user validation policy
图13-9. 由自定义用户验证显示的错误消息

13.4 Completing the Administration Features
13.4 完成管理特性

I only have to implement the features for editing and deleting users to complete my administration tool. In Listing 13-20, you can see the changes I made to the Views/Admin/Index.cshtml file to target Edit and Delete actions in the Admin controller.
为了完成本例的管理工具,我只需实现编辑和删除用户的特性。在清单13-20中可以看到我对Views/Admin/Index.cshtml文件所做修改,它们的目标是Admin控制器中的EditDelete方法。

Listing 13-20. Adding Edit and Delete Buttons to the Index.cshtml File
清单13-20. 在Index.cshtml文件中添加“Edit”和“Delete”按钮

@using Users.Models
@model IEnumerable<AppUser>
@{ ViewBag.Title = "Index"; }
<div class="panel panel-primary">
    <div class="panel-heading">
        User Accounts
    </div>
    <table class="table table-striped"> 
        <tr><th>ID</th><th>Name</th><th>Email</th><th></th></tr> 
        @if (Model.Count() == 0) {
            <tr><td colspan="4" class="text-center">No User Accounts</td></tr> 
        } else {
            foreach (AppUser user in Model) {
                <tr>
                    <td>@user.Id</td>
                    <td>@user.UserName</td>
                    <td>@user.Email</td>
                    <td>
                        @using (Html.BeginForm("Delete", "Admin",
                                    new { id = user.Id })) {
                            @Html.ActionLink("Edit", "Edit", new { id = user.Id },
                                    new { @class = "btn btn-primary btn-xs" })
                            <button class="btn btn-danger btn-xs" type="submit">
                                Delete
                            </button>
                        }
                    </td> 
                </tr>
            }
        }
    </table>
</div>
@Html.ActionLink("Create", "Create", null, new { @class = "btn btn-primary" })

Tip You will notice that I have put the Html.ActionLink call that targets the Edit action method inside the scope of the Html.Begin helper. I did this solely so that the Bootstrap styles will style both elements as buttons displayed on a single line.
提示:你可能会注意到,我在Html.Begin辅助器的范围内放入了一个Html.ActionLink调用,其目标为Edit动作方法。这么做纯粹是为了让Bootstrap将两个元素的样式作为按钮显示成一行。

13.4.1 Implementing the Delete Feature
13.4.1 实现Delete特性

The user manager class defines a DeleteAsync method that takes an instance of the user class and removes it from the database. In Listing 13-21, you can see how I have used the DeleteAsync method to implement the delete feature of the Admin controller.
用户管理器类定义了一个DeleteAsync方法,它以用户类实例为参数,并将其从数据库删除。在清单13-21中,可以看到如何用DeleteAsync方法来实现Admin控制器的删除特性。

Listing 13-21. Deleting Users in the AdminController.cs File
清单13-21. AdminController.cs文件中的删除用户

using System.Web;
using System.Web.Mvc;
using Microsoft.AspNet.Identity.Owin;
using Users.Infrastructure;
using Users.Models;
using Microsoft.AspNet.Identity;
using System.Threading.Tasks; 
namespace Users.Controllers { public class AdminController : Controller {
// ...other action methods omitted for brevity... // ...出于简化,这里忽略了其他方法...
[HttpPost] public async Task<ActionResult> Delete(string id) { AppUser user = await UserManager.FindByIdAsync(id); if (user != null) { IdentityResult result = await UserManager.DeleteAsync(user); if (result.Succeeded) { return RedirectToAction("Index"); } else { return View("Error", result.Errors); } } else { return View("Error", new string[] { "User Not Found" }); } }
private void AddErrorsFromResult(IdentityResult result) { foreach (string error in result.Errors) { ModelState.AddModelError("", error); } }
private AppUserManager UserManager { get { return HttpContext.GetOwinContext().GetUserManager<AppUserManager>(); } } } }

My action method receives the unique ID for the user as an argument, and I use the FindByIdAsync method to locate the corresponding user object so that I can pass it to DeleteAsync method. The result of the DeleteAsync method is an IdentityResult, which I process in the same way I did in earlier examples to ensure that any errors are displayed to the user. You can test the delete functionality by creating a new user and then clicking the Delete button that appears alongside it in the Index view.
该动作方法以用户的唯一ID作为参数,并且使用FindByIdAsync方法定位了相应的用户对象,以便将该对象传递给DeleteAsync方法。DeleteAsync方法的结果是一个IdentityResult,对它的处理方式与之前示例的方式相同,以确保将任何错误都显示给用户。为了测试这个删除功能,可以创建一个新的用户,然后在Index视图中点击出现在该用户旁边的“Delete”按钮。

There is no view associated with the Delete action, so to display any errors I created a view file called Error.cshtml in the Views/Shared folder, the contents of which are shown in Listing 13-22.
Delete动作没有相关联的视图,因此,为了将错误显示出来,我在Views/Shared文件夹中创建了一个视图文件,名称为Error.cshtml,其内容如清单13-22所示。

Listing 13-22. The Contents of the Error.cshtml File
清单13-22. Error.cshtml文件的内容

@model IEnumerable<string>
@{ ViewBag.Title = "Error";}
<div class="alert alert-danger">
    @switch (Model.Count()) {
        case 0:
            @: Something went wrong. Please try again
            break;
        case 1:
            @Model.First();
            break;
        default:
            @: The following errors were encountered:
                <ul>
                    @foreach (string error in Model) {
                        <li>@error</li>
                    }
                </ul>
            break;
    }
</div>
@Html.ActionLink("OK", "Index", null, new { @class = "btn btn-default" })

Tip I put this view in the Views/Shared folder so that it can be used by other controllers, including the one I create to manage roles and role membership in Chapter 14.
提示:我将此视图放在Views/Shared文件夹中,是为了其他控制器也能够使用它,包括第14章为了管理角色及其成员要创建的控制器。

13.4.2 Implementing the Edit Feature
13.4.2 实现Edit特性

To complete the administration tool, I need to add support for editing the e-mail address and password for a user account. These are the only properties defined by users at the moment, but I'll show you how to extend the schema with custom properties in Chapter 15. Listing 13-23 shows the Edit action methods that I added to the Admin controller.
为了完成用户管理工具,我需要为编辑用户账号的E-mail地址和口令的支持。此时这些是用户定义仅有的属性,不过到第15章时,将演示如何扩展数据库架构,使用户带有自定义属性。清单13-23显示了添加到Admin控制器中的Edit动作方法。

Listing 13-23. Adding the Edit Actions in the AdminController.cs File
清单13-23. 在AdminController.cs文件中添加Edit动作

using System.Web;
using System.Web.Mvc;
using Microsoft.AspNet.Identity.Owin;
using Users.Infrastructure;
using Users.Models;
using Microsoft.AspNet.Identity;
using System.Threading.Tasks; 
namespace Users.Controllers { public class AdminController : Controller {
// ...other action methods omitted for brevity... // ...出于简化,这里忽略了其他动作方法...
public async Task<ActionResult> Edit(string id) { AppUser user = await UserManager.FindByIdAsync(id); if (user != null) { return View(user); } else { return RedirectToAction("Index"); } }
[HttpPost] public async Task<ActionResult> Edit(string id, string email, string password) { AppUser user = await UserManager.FindByIdAsync(id); if (user != null) { user.Email = email; IdentityResult validEmail = await UserManager.UserValidator.ValidateAsync(user); if (!validEmail.Succeeded) { AddErrorsFromResult(validEmail); } IdentityResult validPass = null; if (password != string.Empty) { validPass = await UserManager.PasswordValidator.ValidateAsync(password); if (validPass.Succeeded) { user.PasswordHash = UserManager.PasswordHasher.HashPassword(password); } else { AddErrorsFromResult(validPass); } } if ((validEmail.Succeeded && validPass == null) || ( validEmail.Succeeded && password != string.Empty && validPass.Succeeded)) { IdentityResult result = await UserManager.UpdateAsync(user); if (result.Succeeded) { return RedirectToAction("Index"); } else { AddErrorsFromResult(result); } } } else { ModelState.AddModelError("", "User Not Found"); } return View(user); }

private void AddErrorsFromResult(IdentityResult result) { foreach (string error in result.Errors) { ModelState.AddModelError("", error); } }
private AppUserManager UserManager { get { return HttpContext.GetOwinContext().GetUserManager<AppUserManager>(); } } } }

The Edit action targeted by GET requests uses the ID string embedded in the Index view to call the FindByIdAsync method in order to get an AppUser object that represents the user.
由GET请求作为目标的Edit动作使用Index视图中的ID字符串调用FindByIdAsync方法,目的是获得表示该用户的AppUser对象。

The more complex implementation receives the POST request, with arguments for the user ID, the new e-mail address, and the password. I have to perform several tasks to complete the editing operation.
那个较复杂的实现接收POST请求,以用户ID、新E-mail地址以及口令为参数。我必须执行几个任务才能完成这种编辑操作。

The first task is to validate the values I have received. I am working with a simple user object at the moment—although I'll show you how to customize the data stored for users in Chapter 15—but even so, I need to validate the user data to ensure that I don't violate the custom policies defined in the “Validating User Details” and “Validating Passwords” sections. I start by validating the e-mail address, which I do like this:
第一个任务是验证接收到的值。此时使用的是一个简单的用户对象——尽管第15章将演示如何自定义存储用户的数据——但即使如此,还是需要验证用户数据,以确保不会与“验证用户细节”和“验证口令”小节中定义的自定义策略相冲突。首先验证E-mail地址,做法如下:

...
user.Email = email;
IdentityResult validEmail = await UserManager.UserValidator.ValidateAsync(user); 
if (!validEmail.Succeeded) {
    AddErrorsFromResult(validEmail);
}
...

Tip Notice that I have to change the value of the Email property before I perform the validation because the ValidateAsync method only accepts instances of the user class.
提示:注意,在执行验证之前,必须修改Email属性的值,因为ValidateAsync方法只接收用户类的实例。

The next step is to change the password, if one has been supplied. ASP.NET Identity stores hashes of passwords, rather than the passwords themselves—this is intended to prevent passwords from being stolen. My next step is to take the validated password and generate the hash code that will be stored in the database so that the user can be authenticated (which I demonstrate in Chapter 14).
下一个步骤是在已提供口令时,修改用户口令。ASP.NET Identity存储的是口令的哈希值,而不是口令本身——目的是防止口令被窃取。我的下一个步骤是取得这个已验证的口令,并生成将被存储到数据库中的哈希码,以便让用户能够被认证,这将在第14章演示。

Passwords are converted to hashes through an implementation of the IPasswordHasher interface, which is obtained through the AppUserManager.PasswordHasher property. The IPasswordHasher interface defines the HashPassword method, which takes a string argument and returns its hashed value, like this:
将口令转换成哈希值是通过IPasswordHasher接口的实现来做的,该接口实现是通过AppUserManager.PasswordHasher属性获得的。IPasswordHasher接口定义了HashPassword方法,它以一个字符串为参数,返回该字符串的哈希值,如下所示:

...
if (password != string.Empty) {
    validPass = await UserManager.PasswordValidator.ValidateAsync(password); 
    if (validPass.Succeeded) {
        user.PasswordHash = UserManager.PasswordHasher.HashPassword(password); 
    } else {
        AddErrorsFromResult(validPass);
    }
}
...

Changes to the user class are not stored in the database until the UpdateAsync method is called, like this:
对用户类的修改直到调用UpdateAsync方法时,才会存储到数据库中,如下所示:

...
if ((validEmail.Succeeded && validPass == null)
        || ( validEmail.Succeeded && password != string.Empty &&
        validPass.Succeeded)) {
    IdentityResult result = await UserManager.UpdateAsync(user); 
    if (result.Succeeded) {
        return RedirectToAction("Index");
    } else {
        AddErrorsFromResult(result);
    }
}
...

1. Creating the View
1. 创建视图

The final component is the view that will render the current values for a user and allow new values to be submitted to the controller. Listing 13-24 shows the contents of the Views/Admin/Edit.cshtml file.
最后一个组件是渲染当前用户值并将新值递交给控制器的视图。清单13-24显示了Views/Admin/Edit.cshtml文件的内容。

Listing 13-24. The Contents of the Edit.cshtml File
清单13-24. Edit.cshtml文件的内容

@model Users.Models.AppUser
@{ ViewBag.Title = "Edit"; }
@Html.ValidationSummary(false)
<h2>Edit User</h2> 
<div class="form-group"> <label>Name</label> <p class="form-control-static">@Model.Id</p> </div> @using (Html.BeginForm()) { @Html.HiddenFor(x => x.Id) <div class="form-group"> <label>Email</label> @Html.TextBoxFor(x => x.Email, new { @class = "form-control" }) </div> <div class="form-group"> <label>Password</label> <input name="password" type="password" class="form-control" /> </div> <button type="submit" class="btn btn-primary">Save</button> @Html.ActionLink("Cancel", "Index", null, new { @class = "btn btn-default" }) }

There is nothing special about the view. It displays the user ID, which cannot be changed, as static text and provides a form for editing the e-mail address and password, as shown in Figure 13-10. Validation problems are displayed in the validation summary section of the view, and successfully editing a user account will return to the list of accounts in the system.
该视图没什么特别的。它将用户ID显示成静态文本,这是不能修改的,并且提供了一个对E-mail地址和口令进行编辑的表单,如图13-10所示。验证问题会显示在视图的验证摘要处,当成功编辑一个用户账号时,会返回到系统的账号列表。

图13-10

Figure 13-10. Editing a user account
图13-10. 编辑用户账号

13.5 Summary
13.5 小结

In this chapter, I showed you how to create the configuration and classes required to use ASP.NET Identity and demonstrated how they can be applied to create a user administration tool. In the next chapter, I show you how to perform authentication and authorization with ASP.NET Identity.
在本章中,我演示了如何创建使用ASP.NET Identity所需的配置和类,并且演示了如何运用它们来创建用户管理工具。下一章将演示如何用ASP.NET Identity执行认证与授权。


看完此文如果觉得有所收获,请给个推荐
你的推荐是我继续下去的动力,也会让更多人关注并获益,这也是你的贡献。

posted @ 2016-02-02 17:18  r01cn  阅读(18766)  评论(30编辑  收藏  举报