ASP.NET Identity系列教程Identity高级技术

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ASP.NET Identity系列教程Identity高级技术相关的知识,希望对你有一定的参考价值。

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

15 Advanced ASP.NET Identity
15 ASP.NET Identity高级技术

In this chapter, I finish my description of ASP.NET Identity by showing you some of the advanced features it offers. I demonstrate how you can extend the database schema by defining custom properties on the user class and how to use database migrations to apply those properties without deleting the data in the ASP.NET Identity database. I also explain how ASP.NET Identity supports the concept of claims and demonstrates how they can be used to flexibly authorize access to action methods. I finish the chapter—and the book—by showing you how ASP.NET Identity makes it easy to authenticate users through third parties. I demonstrate authentication with Google accounts, but ASP.NET Identity has built-in support for Microsoft, Facebook, and Twitter accounts as well. Table 15-1 summarizes this chapter.
本章将完成对ASP.NET Identity的描述,向你展示它所提供的一些高级特性。我将演示,你可以扩展ASP.NET Identity的数据库架构,其办法是在用户类上定义一些自定义属性。也会演示如何使用数据库迁移,这样可以运用自定义属性,而不必删除ASP.NET Identity数据库中的数据。还会解释ASP.NET Identity如何支持声明(Claims)概念,并演示如何将它们灵活地用来对动作方法进行授权访问。最后向你展示ASP.NET Identity很容易通过第三方部件来认证用户,以此结束本章以及本书。将要演示的是使用Google账号认证,但ASP.NET Identity对于Microsoft、Facebook以及Twitter账号,都有内建的支持。表15-1是本章概要。

Table 15-1. Chapter Summary
表15-1. 本章概要
Problem
问题
Solution
解决方案
Listing
清单号
Store additional information about users.
存储用户的附加信息
Define custom user properties.
定义自定义用户属性
1–3, 8–11
Update the database schema without deleting user data.
更新数据库架构而不删除用户数据
Perform a database migration.
执行数据库迁移
4–7
Perform fine-grained authorization.
执行细粒度授权
Use claims.
使用声明(Claims)
12–14
Add claims about a user.
添加用户的声明(Claims)
Use the ClaimsIdentity.AddClaims method.
使用ClaimsIdentity.AddClaims方法
15–19
Authorize access based on claim values.
基于声明(Claims)值授权访问
Create a custom authorization filter attribute.
创建一个自定义的授权过滤器注解属性
20–21
Authenticate through a third party.
通过第三方认证
Install the NuGet package for the authentication provider, redirect requests to that provider, and specify a callback URL that creates the user account.
安装认证提供器的NuGet包,将请求重定向到该提供器,并指定一个创建用户账号的回调URL。
22–25

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

In this chapter, I am going to continue working on the Users project I created in Chapter 13 and enhanced in Chapter 14. No changes to the application are required, but start the application and make sure that there are users in the database. Figure 15-1 shows the state of my database, which contains the users Admin, Alice, Bob, and Joe from the previous chapter. To check the users, start the application and request the /Admin/Index URL and authenticate as the Admin user.
本章打算继续使用第13章创建并在第14章增强的Users项目。对应用程序无需做什么改变,但需要启动应用程序,并确保数据库中有一些用户。图15-1显示了数据库的状态,它含有上一章的用户AdminAliceBob以及Joe。为了检查用户,请启动应用程序,请求/Admin/Index URL,并以Admin用户进行认证。

技术分享

Figure 15-1. The initial users in the Identity database
图15-1. Identity数据库中的最初用户

I also need some roles for this chapter. I used the RoleAdmin controller to create roles called Users and Employees and assigned the users to those roles, as described in Table 15-2.
本章还需要一些角色。我用RoleAdmin控制器创建了角色UsersEmployees,并为这些角色指定了一些用户,如表15-2所示。

Table 15-2. The Types of Web Forms Code Nuggets
表15-2. 角色及成员(作者将此表的标题写错了——译者注)
Role
角色
Members
成员
Users Alice, Joe
Employees Alice, Bob

Figure 15-2 shows the required role configuration displayed by the RoleAdmin controller.
图15-2显示了由RoleAdmin控制器所显示出来的必要的角色配置。

技术分享

Figure 15-2. Configuring the roles required for this chapter
图15-2. 配置本章所需的角色

15.2 Adding Custom User Properties
15.2 添加自定义用户属性

When I created the AppUser class to represent users in Chapter 13, I noted that the base class defined a basic set of properties to describe the user, such as e-mail address and telephone number. Most applications need to store more information about users, including persistent application preferences and details such as addresses—in short, any data that is useful to running the application and that should last between sessions. In ASP.NET Membership, this was handled through the user profile system, but ASP.NET Identity takes a different approach.
我在第13章创建AppUser类来表示用户时曾做过说明,基类定义了一组描述用户的基本属性,如E-mail地址、电话号码等。大多数应用程序还需要存储用户的更多信息,包括持久化应用程序爱好以及地址等细节——简言之,需要存储对运行应用程序有用并且在各次会话之间应当保持的任何数据。在ASP.NET Membership中,这是通过用户资料(User Profile)系统来处理的,但ASP.NET Identity采取了一种不同的办法。

Because the ASP.NET Identity system uses Entity Framework to store its data by default, defining additional user information is just a matter of adding properties to the user class and letting the Code First feature create the database schema required to store them. Table 15-3 puts custom user properties in context.
因为ASP.NET Identity默认是使用Entity Framework来存储其数据的,定义附加的用户信息只不过是给用户类添加属性的事情,然后让Code First特性去创建需要存储它们的数据库架构即可。表15-3描述了自定义用户属性的情形。

Table 15-3. Putting Cusotm User Properties in Context
表15-3. 自定义用户属性的情形
Question
问题
Answer
回答
What is it?
什么是自定义用户属性?
Custom user properties allow you to store additional information about your users, including their preferences and settings.
自定义用户属性让你能够存储附加的用户信息,包括他们的爱好和设置。
Why should I care?
为何要关心它?
A persistent store of settings means that the user doesn’t have to provide the same information each time they log in to the application.
设置的持久化存储意味着,用户不必每次登录到应用程序时都提供同样的信息。
How is it used by the MVC framework?
在MVC框架中如何使用它?
This feature isn’t used directly by the MVC framework, but it is available for use in action methods.
此特性不是由MVC框架直接使用的,但它在动作方法中使用是有效的。

15.2.1 Defining Custom Properties
15.2.1 定义自定义属性

Listing 15-1 shows how I added a simple property to the AppUser class to represent the city in which the user lives.
清单15-1演示了如何给AppUser类添加一个简单的属性,用以表示用户生活的城市。

Listing 15-1. Adding a Property in the AppUser.cs File
清单15-1. 在AppUser.cs文件中添加属性

using System;
using Microsoft.AspNet.Identity.EntityFramework;
namespace Users.Models { public enum Cities { LONDON, PARIS, CHICAGO }
public class AppUser : IdentityUser { public Cities City { get; set; } }
}

I have defined an enumeration called Cities that defines values for some large cities and added a property called City to the AppUser class. To allow the user to view and edit their City property, I added actions to the Home controller, as shown in Listing 15-2.
这里定义了一个枚举,名称为Cities,它定义了一些大城市的值,另外给AppUser类添加了一个名称为City的属性。为了让用户能够查看和编辑City属性,给Home控制器添加了几个动作方法,如清单15-2所示。

Listing 15-2. Adding Support for Custom User Properties in the HomeController.cs File
清单15-2. 在HomeController.cs文件中添加对自定义属性的支持

using System.Web.Mvc;
using System.Collections.Generic;
using System.Web;
using System.Security.Principal;
using System.Threading.Tasks;
using Users.Infrastructure;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Users.Models;
namespace Users.Controllers {
public class HomeController : Controller {
[Authorize] public ActionResult Index() { return View(GetData("Index")); }
[Authorize(Roles = "Users")] public ActionResult OtherAction() { return View("Index", GetData("OtherAction")); }
private Dictionary<string, object> GetData(string actionName) { Dictionary<string, object> dict = new Dictionary<string, object>(); dict.Add("Action", actionName); dict.Add("User", HttpContext.User.Identity.Name); dict.Add("Authenticated", HttpContext.User.Identity.IsAuthenticated); dict.Add("Auth Type", HttpContext.User.Identity.AuthenticationType); dict.Add("In Users Role", HttpContext.User.IsInRole("Users")); return dict; }
[Authorize] public ActionResult UserProps() { return View(CurrentUser); }
[Authorize] [HttpPost] public async Task<ActionResult> UserProps(Cities city) { AppUser user = CurrentUser; user.City = city; await UserManager.UpdateAsync(user); return View(user); }
private AppUser CurrentUser { get { return UserManager.FindByName(HttpContext.User.Identity.Name); } }
private AppUserManager UserManager { get { return HttpContext.GetOwinContext().GetUserManager<AppUserManager>(); } }
} }

I added a CurrentUser property that uses the AppUserManager class to retrieve an AppUser instance to represent the current user. I pass the AppUser object as the view model object in the GET version of the UserProps action method, and the POST method uses it to update the value of the new City property. Listing 15-3 shows the UserProps.cshtml view, which displays the City property value and contains a form to change it.
我添加了一个CurrentUser属性,它使用AppUserManager类接收了表示当前用户的AppUser实例。在GET版本的UserProps动作方法中,传递了这个AppUser对象作为视图模型。而在POST版的方法中用它更新了City属性的值。清单15-3显示了UserProps.cshtml视图,它显示了City属性的值,并包含一个修改它的表单。

Listing 15-3. The Contents of the UserProps.cshtml File in the Views/Home Folder
清单15-3. Views/Home文件夹中UserProps.cshtml文件的内容

@using Users.Models
@model AppUser
@{ ViewBag.Title = "UserProps";}
<div class="panel panel-primary"> <div class="panel-heading"> Custom User Properties </div> <table class="table table-striped"> <tr><th>City</th><td>@Model.City</td></tr> </table> </div>
@using (Html.BeginForm()) { <div class="form-group"> <label>City</label> @Html.DropDownListFor(x => x.City, new SelectList(Enum.GetNames(typeof(Cities)))) </div> <button class="btn btn-primary" type="submit">Save</button> }

Caution Don’t start the application when you have created the view. In the sections that follow, I demonstrate how to preserve the contents of the database, and if you start the application now, the ASP.NET Identity users will be deleted.
警告:创建了视图之后不要启动应用程序。在以下小节中,将演示如何保留数据库的内容,如果现在启动应用程序,将会删除ASP.NET Identity的用户。

15.2.2 Preparing for Database Migration
15.2.2 准备数据库迁移

The default behavior for the Entity Framework Code First feature is to drop the tables in the database and re-create them whenever classes that drive the schema have changed. You saw this in Chapter 14 when I added support for roles: When the application was started, the database was reset, and the user accounts were lost.
Entity Framework Code First特性的默认行为是,一旦修改了派生数据库架构的类,便会删除数据库中的数据表,并重新创建它们。在第14章可以看到这种情况,在我添加角色支持时:当重启应用程序后,数据库被重置,用户账号也丢失。

Don’t start the application yet, but if you were to do so, you would see a similar effect. Deleting data during development is usually not a problem, but doing so in a production setting is usually disastrous because it deletes all of the real user accounts and causes a panic while the backups are restored. In this section, I am going to demonstrate how to use the database migration feature, which updates a Code First schema in a less brutal manner and preserves the existing data it contains.
不要启动应用程序,但如果你这么做了,会看到类似的效果。在开发期间删除数据没什么问题,但如果在产品设置中这么做了,通常是灾难性的,因为它会删除所有真实的用户账号,而备份恢复是很痛苦的事。在本小节中,我打算演示如何使用数据库迁移特性,它能以比较温和的方式更新Code First的架构,并保留架构中的已有数据。

The first step is to issue the following command in the Visual Studio Package Manager Console:
第一个步骤是在Visual Studio的“Package Manager Console(包管理器控制台)”中发布以下命令:

Enable-Migrations –EnableAutomaticMigrations

This enables the database migration support and creates a Migrations folder in the Solution Explorer that contains a Configuration.cs class file, the contents of which are shown in Listing 15-4.
它启用了数据库的迁移支持,并在“Solution Explorer(解决方案资源管理器)”创建一个Migrations文件夹,其中含有一个Configuration.cs类文件,内容如清单15-4所示。

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

namespace Users.Migrations {
using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;
internal sealed class Configuration : DbMigrationsConfiguration< Users.Infrastructure.AppIdentityDbContext> { public Configuration() { AutomaticMigrationsEnabled = true; ContextKey = "Users.Infrastructure.AppIdentityDbContext"; }
protected override void Seed(Users.Infrastructure.AppIdentityDbContext context) { // This method will be called after migrating to the latest version. // 此方法将在迁移到最新版本时调用
// You can use the DbSet<T>.AddOrUpdate() helper extension method // to avoid creating duplicate seed data. E.g. // 例如,你可以使用DbSet<T>.AddOrUpdate()辅助器方法来避免创建重复的种子数据 // // context.People.AddOrUpdate( // p => p.FullName, // new Person { FullName = "Andrew Peters" }, // new Person { FullName = "Brice Lambson" }, // new Person { FullName = "Rowan Miller" } // ); // } } }

Tip You might be wondering why you are entering a database migration command into the console used to manage NuGet packages. The answer is that the Package Manager Console is really PowerShell, which is a general-purpose tool that is mislabeled by Visual Studio. You can use the console to issue a wide range of helpful commands. See http://go.microsoft.com/fwlink/?LinkID=108518 for details.
提示:你可能会觉得奇怪,为什么要在管理NuGet包的控制台中输入数据库迁移的命令?答案是“Package Manager Console(包管理控制台)”是真正的PowerShell,这是Visual studio冒用的一个通用工具。你可以使用此控制台发送大量的有用命令,详见http://go.microsoft.com/fwlink/?LinkID=108518。

The class will be used to migrate existing content in the database to the new schema, and the Seed method will be called to provide an opportunity to update the existing database records. In Listing 15-5, you can see how I have used the Seed method to set a default value for the new City property I added to the AppUser class. (I have also updated the class file to reflect my usual coding style.)
这个类将用于把数据库中的现有内容迁移到新的数据库架构,Seed方法的调用为更新现有数据库记录提供了机会。在清单15-5中可以看到,我如何用Seed方法为新的City属性设置默认值,City是添加到AppUser类中自定义属性。(为了体现我一贯的编码风格,我对这个类文件也进行了更新。)

Listing 15-5. Managing Existing Content in the Configuration.cs File
清单15-5. 在Configuration.cs文件中管理已有内容

using System.Data.Entity.Migrations;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Users.Infrastructure;
using Users.Models;
namespace Users.Migrations {
internal sealed class Configuration : DbMigrationsConfiguration<AppIdentityDbContext> {
public Configuration() { AutomaticMigrationsEnabled = true; ContextKey = "Users.Infrastructure.AppIdentityDbContext"; }
protected override void Seed(AppIdentityDbContext context) {
AppUserManager userMgr = new AppUserManager(new UserStore<AppUser>(context)); AppRoleManager roleMgr = new AppRoleManager(new RoleStore<AppRole>(context));
string roleName = "Administrators"; string userName = "Admin"; string password = "MySecret"; string email = "[email protected]";
if (!roleMgr.RoleExists(roleName)) { roleMgr.Create(new AppRole(roleName)); }
AppUser user = userMgr.FindByName(userName); if (user == null) { userMgr.Create(new AppUser { UserName = userName, Email = email }, password); user = userMgr.FindByName(userName); }
if (!userMgr.IsInRole(user.Id, roleName)) { userMgr.AddToRole(user.Id, roleName); }
foreach (AppUser dbUser in userMgr.Users) { dbUser.City = Cities.PARIS; } context.SaveChanges();
} } }

You will notice that much of the code that I added to the Seed method is taken from the IdentityDbInit class, which I used to seed the database with an administration user in Chapter 14. This is because the new Configuration class added to support database migrations will replace the seeding function of the IdentityDbInit class, which I’ll update shortly. Aside from ensuring that there is an admin user, the statements in the Seed method that are important are the ones that set the initial value for the City property I added to the AppUser class, as follows:
你可能会注意到,添加到Seed方法中的许多代码取自于IdentityDbInit类,在第14章中我用这个类将管理用户植入了数据库。这是因为这个新添加的、用以支持数据库迁移的Configuration类,将代替IdentityDbInit类的种植功能,我很快便会更新这个类。除了要确保有admin用户之外,在Seed方法中的重要语句是那些为AppUser类的City属性设置初值的语句,如下所示:

...
foreach (AppUser dbUser in userMgr.Users) {
     dbUser.City = Cities.PARIS;
}
context.SaveChanges();
...

You don’t have to set a default value for new properties—I just wanted to demonstrate that the Seed method in the Configuration class can be used to update the existing user records in the database.
你不一定要为新属性设置默认值——这里只是想演示Configuration类中的Seed方法,可以用它更新数据库中的已有用户记录。

Caution Be careful when setting values for properties in the Seed method for real projects because the values will be applied every time you change the schema, overriding any values that the user has set since the last schema update was performed. I set the value of the City property just to demonstrate that it can be done.
警告:在用于真实项目的Seed方法中为属性设置值时要小心,因为你每一次修改架构时,都会运用这些值,这会将自执行上一次架构更新之后,用户设置的任何数据覆盖掉。这里设置City属性的值只是为了演示它能够这么做。

Changing the Database Context Class
修改数据库上下文类

The reason that I added the seeding code to the Configuration class is that I need to change the IdentityDbInit class. At present, the IdentityDbInit class is derived from the descriptively named DropCreateDatabaseIfModelChanges<AppIdentityDbContext> class, which, as you might imagine, drops the entire database when the Code First classes change. Listing 15-6 shows the changes I made to the IdentityDbInit class to prevent it from affecting the database.
Configuration类中添加种植代码的原因是我需要修改IdentityDbInit类。此时,IdentityDbInit类派生于描述性命名的DropCreateDatabaseIfModelChanges<AppIdentityDbContext> 类,和你相像的一样,它会在Code First类改变时删除整个数据库。清单15-6显示了我对IdentityDbInit类所做的修改,以防止它影响数据库。

Listing 15-6. Preventing Database Schema Changes in the AppIdentityDbContext.cs File
清单15-6. 在AppIdentityDbContext.cs文件是阻止数据库架构变化

using System.Data.Entity;
using Microsoft.AspNet.Identity.EntityFramework;
using Users.Models;
using Microsoft.AspNet.Identity; 
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 : NullDatabaseInitializer<AppIdentityDbContext> { } }

I have removed the methods defined by the class and changed its base to NullDatabaseInitializer<AppIdentityDbContext> , which prevents the schema from being altered.
我删除了这个类中所定义的方法,并将它的基类改为NullDatabaseInitializer<AppIdentityDbContext> ,它可以防止架构修改。

15.2.3 Performing the Migration
15.2.3 执行迁移

All that remains is to generate and apply the migration. First, run the following command in the Package Manager Console:
剩下的事情只是生成并运用迁移了。首先,在“Package Manager Console(包管理器控制台)”中执行以下命令:

Add-Migration CityProperty

This creates a new migration called CityProperty (I like my migration names to reflect the changes I made). A class new file will be added to the Migrations folder, and its name reflects the time at which the command was run and the name of the migration. My file is called 201402262244036_CityProperty.cs, for example. The contents of this file contain the details of how Entity Framework will change the database during the migration, as shown in Listing 15-7.
这创建了一个名称为CityProperty的新迁移(我比较喜欢让迁移的名称反映出我所做的修改)。这会在文件夹中添加一个新的类文件,而且其命名会反映出该命令执行的时间以及迁移名称,例如,我的这个文件名称为201402262244036_CityProperty.cs。该文件的内容含有迁移期间Entity Framework修改数据库的细节,如清单15-7所示。

Listing 15-7. The Contents of the 201402262244036_CityProperty.cs File
清单15-7. 201402262244036_CityProperty.cs文件的内容

namespace Users.Migrations {
    using System;
    using System.Data.Entity.Migrations; 
public partial class Init : DbMigration { public override void Up() { AddColumn("dbo.AspNetUsers", "City", c => c.Int(nullable: false)); }
public override void Down() { DropColumn("dbo.AspNetUsers", "City"); } } }

The Up method describes the changes that have to be made to the schema when the database is upgraded, which in this case means adding a City column to the AspNetUsers table, which is the one that is used to store user records in the ASP.NET Identity database.
Up方法描述了在数据库升级时,需要对架构所做的修改,在这个例子中,意味着要在AspNetUsers数据表中添加City数据列,该数据表是ASP.NET Identity数据库用来存储用户记录的。

The final step is to perform the migration. Without starting the application, run the following command in the Package Manager Console:
最后一步是执行迁移。无需启动应用程序,只需在“Package Manager Console(包管理器控制台)”中运行以下命令即可:

Update-Database –TargetMigration CityProperty

The database schema will be modified, and the code in the Configuration.Seed method will be executed. The existing user accounts will have been preserved and enhanced with a City property (which I set to Paris in the Seed method).
这会修改数据库架构,并执行Configuration.Seed方法中的代码。已有用户账号会被保留,且增强了City属性(我在Seed方法中已将其设置为“Paris”)。

15.2.4 Testing the Migration
15.2.4 测试迁移

To test the effect of the migration, start the application, navigate to the /Home/UserProps URL, and authenticate as one of the Identity users (for example, as Alice with the password MySecret). Once authenticated, you will see the current value of the City property for the user and have the opportunity to change it, as shown in Figure 15-3.
为了测试迁移的效果,启动应用程序,导航到/Home/UserProps URL,并以Identity中的用户(例如Alice,口令MySecret)进行认证。一旦已被认证,便会看到该用户City属性的当前值,并可以对其进行修改,如图15-3所示。

技术分享

Figure 15-3. Displaying and changing a custom user property
图15-3. 显示和个性自定义用户属性

15.2.5 Defining an Additional Property
15.2.5 定义附加属性

Now that database migrations are set up, I am going to define a further property just to demonstrate how subsequent changes are handled and to show a more useful (and less dangerous) example of using the Configuration.Seed method. Listing 15-8 shows how I added a Country property to the AppUser class.
现在,已经建立了数据库迁移,我打算再定义一个属性,这恰恰演示了如何处理持续不断的修改,也为了演示Configuration.Seed方法更有用(至少无害)的示例。清单15-8显示了我在AppUser类上添加了一个Country属性。

Listing 15-8. Adding Another Property in the AppUserModels.cs File
清单15-8. 在AppUserModels.cs文件中添加另一个属性

using System;
using Microsoft.AspNet.Identity.EntityFramework; 
namespace Users.Models {
public enum Cities { LONDON, PARIS, CHICAGO }
public enum Countries { NONE, UK, FRANCE, USA }
public class AppUser : IdentityUser { public Cities City { get; set; } public Countries Country { get; set; }
public void SetCountryFromCity(Cities city) { switch (city) { case Cities.LONDON: Country = Countries.UK; break; case Cities.PARIS: Country = Countries.FRANCE; break; case Cities.CHICAGO: Country = Countries.USA; break; default: Country = Countries.NONE; break; }
} } }

I have added an enumeration to define the country names and a helper method that selects a country value based on the City property. Listing 15-9 shows the change I made to the Configuration class so that the Seed method sets the Country property based on the City, but only if the value of Country is NONE (which it will be for all users when the database is migrated because the Entity Framework sets enumeration columns to the first value).
我已经添加了一个枚举,它定义了国家名称。还添加了一个辅助器方法,它可以根据City属性选择一个国家。清单15-9显示了对Configuration类所做的修改,以使Seed方法根据City设置Country属性,但只当CountryNONE时才进行设置(在迁移数据库时,所有用户都是NONE,因为Entity Framework会将枚举列设置为枚举的第一个值)。

Listing 15-9. Modifying the Database Seed in the Configuration.cs File
清单15-9. 在Configuration.cs文件中修改数据库种子

using System.Data.Entity.Migrations;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Users.Infrastructure;
using Users.Models; 
namespace Users.Migrations {
internal sealed class Configuration : DbMigrationsConfiguration<AppIdentityDbContext> {
public Configuration() { AutomaticMigrationsEnabled = true; ContextKey = "Users.Infrastructure.AppIdentityDbContext"; }
protected override void Seed(AppIdentityDbContext context) {
AppUserManager userMgr = new AppUserManager(new UserStore<AppUser>(context)); AppRoleManager roleMgr = new AppRoleManager(new RoleStore<AppRole>(context));
string roleName = "Administrators"; string userName = "Admin"; string password = "MySecret"; string email = "[email protected]";
if (!roleMgr.RoleExists(roleName)) { roleMgr.Create(new AppRole(roleName)); }
AppUser user = userMgr.FindByName(userName); if (user == null) { userMgr.Create(new AppUser { UserName = userName, Email = email }, password); user = userMgr.FindByName(userName); }
if (!userMgr.IsInRole(user.Id, roleName)) { userMgr.AddToRole(user.Id, roleName); }
foreach (AppUser dbUser in userMgr.Users) { if (dbUser.Country == Countries.NONE) { dbUser.SetCountryFromCity(dbUser.City); } }
context.SaveChanges(); } } }

This kind of seeding is more useful in a real project because it will set a value for the Country property only if one has not already been set—subsequent migrations won’t be affected, and user selections won’t be lost.
这种种植在实际项目中会更有用,因为它只会在Country属性未设置时,才会设置Country属性的值——后继的迁移不会受到影响,因此不会失去用户的选择。

1. Adding Application Support
1. 添加应用程序支持

There is no point defining additional user properties if they are not available in the application, so Listing 15-10 shows the change I made to the Views/Home/UserProps.cshtml file to display the value of the Country property.
应用程序中如果没有定义附加属性的地方,则附加属性就无法使用了,因此,清单15-10显示了我对Views/Home/UserProps.cshtml文件的修改,以显示Country属性的值。

Listing 15-10. Displaying an Additional Property in the UserProps.cshtml File
清单15-10. 在UserProps.cshtml文件中显示附加属性

@using Users.Models
@model AppUser
@{ ViewBag.Title = "UserProps";}  
<div class="panel panel-primary">
    <div class="panel-heading">
        Custom User Properties
    </div>
    <table class="table table-striped">
        <tr><th>City</th><td>@Model.City</td></tr>
        <tr><th>Country</th><td>@Model.Country</td></tr>
    </table>
</div>
@using (Html.BeginForm()) {
    <div class="form-group">
        <label>City</label>
        @Html.DropDownListFor(x => x.City, new SelectList(Enum.GetNames(typeof(Cities))))
    </div>
    <button class="btn btn-primary" type="submit">Save</button>
} 

Listing 15-11 shows the corresponding change I made to the Home controller to update the Country property when the City value changes.
为了在City值变化时能够更新Country属性,清单15-11显示了我对Home控制器所做的相应修改。

Listing 15-11. Setting Custom Properties in the HomeController.cs File
清单15-11. 在HomeController.cs文件中设置自定义属性

using System.Web.Mvc;
using System.Collections.Generic;
using System.Web;
using System.Security.Principal;
using System.Threading.Tasks;
using Users.Infrastructure;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Users.Models; 
namespace Users.Controllers {
public class HomeController : Controller { // ...other action methods omitted for brevity... // ...出于简化,这里忽略了其他动作方法...
[Authorize] public ActionResult UserProps() { return View(CurrentUser); }
[Authorize] [HttpPost] public async Task<ActionResult> UserProps(Cities city) { AppUser user = CurrentUser; user.City = city; user.SetCountryFromCity(city); await UserManager.UpdateAsync(user); return View(user); }
// ...properties omitted for brevity... // ...出于简化,这里忽略了一些属性... } }

2. Performing the Migration
2. 准备迁移

All that remains is to create and apply a new migration. Enter the following command into the Package Manager Console:
剩下的事情就是创建和运用新的迁移了。在“Package Manager Console(包管理器控制台)”中输入以下命令:

Add-Migration CountryProperty

This will generate another file in the Migrations folder that contains the instruction to add the Country column. To apply the migration, execute the following command:
这将在Migrations文件夹中生成另一个文件,它含有添加Country数据表列的指令。为了运用迁移,可执行以下命令:

Update-Database –TargetMigration CountryProperty

The migration will be performed, and the value of the Country property will be set based on the value of the existing City property for each user. You can check the new user property by starting the application and authenticating and navigating to the /Home/UserProps URL, as shown in Figure 15-4.
这将执行迁移,Country属性的值将根据每个用户当前的City属性进行设置。通过启动应用程序,认证并导航到/Home/UserProps URL,便可以查看新的用户属性,如图15-4所示。

技术分享

Figure 15-4. Creating an additional user property
图15-4. 创建附加用户属性

Tip Although I am focused on the process of upgrading the database, you can also migrate back to a previous version by specifying an earlier migration. Use the –Force argument make changes that cause data loss, such as removing a column.
提示:虽然我们关注了升级数据库的过程,但你也可以回退到以前的版本,只需指定一个早期的迁移即可。使用-Force参数进行修改,会引起数据丢失,例如删除数据表列。

15.3 Working with Claims
15.3 使用声明(Claims)

In older user-management systems, such as ASP.NET Membership, the application was assumed to be the authoritative source of all information about the user, essentially treating the application as a closed world and trusting the data that is contained within it.
在旧的用户管理系统中,例如ASP.NET Membership,应用程序被假设成是用户所有信息的权威来源,本质上将应用程序视为是一个封闭的世界,并且只信任其中所包含的数据。

This is such an ingrained approach to software development that it can be hard to recognize that’s what is happening, but you saw an example of the closed-world technique in Chapter 14 when I authenticated users against the credentials stored in the database and granted access based on the roles associated with those credentials. I did the same thing again in this chapter when I added properties to the user class. Every piece of information that I needed to manage user authentication and authorization came from within my application—and that is a perfectly satisfactory approach for many web applications, which is why I demonstrated these techniques in such depth.
这是软件开发的一种根深蒂固的方法,使人很难认识到这到底意味着什么,第14章你已看到了这种封闭世界技术的例子,根据存储在数据库中的凭据来认证用户,并根据与凭据关联在一起的角色来授权访问。本章前述在用户类上添加属性,也做了同样的事情。我管理用户认证与授权所需的每一个数据片段都来自于我的应用程序——而且这是许多Web应用程序都相当满意的一种方法,这也是我如此深入地演示这些技术的原因。

ASP.NET Identity also supports an alternative approach for dealing with users, which works well when the MVC framework application isn’t the sole source of information about users and which can be used to authorize users in more flexible and fluid ways than traditional roles allow.
ASP.NET Identity还支持另一种处理用户的办法,当MVC框架的应用程序不是有关用户的唯一信息源时,这种办法会工作得很好,而且能够比传统的角色授权更为灵活且流畅的方式进行授权。

This alternative approach uses claims, and in this section I’ll describe how ASP.NET Identity supports claims-based authorization. Table 15-4 puts claims in context.
这种可选的办法使用了“Claims(声明)”,因此在本小节中,我将描述ASP.NET Identity如何支持“Claims-Based Authorization(基于声明的授权)”。表15-4描述了声明(Claims)的情形。

提示:“Claim”在英文字典中不完全是“声明”的义思,根据本文的描述,感觉把它说成“声明”也不一定合适,所以在之后的译文中基本都写成中英文并用的形式,即“声明(Claims)”。根据表15-4中的声明(Claims)的定义:声明(Claims)是关于用户的一些信息片段。一个用户的信息片段当然有很多,每一个信息片段就是一项声明(Claim),用户的所有信息片段合起来就是该用户的声明(Claims)。请读者注意该单词的单复数形式——译者注

Table 15-4. Putting Claims in Context
表15-4. 声明(Claims)的情形
Question
问题
Answer
答案
What is it?
什么是声明(Claims)?
Claims are pieces of information about users that you can use to make authorization decisions. Claims can be obtained from external systems as well as from the local Identity database.
声明(Claims)是关于用户的一些信息片段,可以用它们做出授权决定。声明(Claims)可以从外部系统获取,也可以从本地的Identity数据库获取。
Why should I care?
为何要关心它?
Claims can be used to flexibly authorize access to action methods. Unlike conventional roles, claims allow access to be driven by the information that describes the user.
声明(Claims)可以用来对动作方法进行灵活的授权访问。与传统的角色不同,声明(Claims)让访问能够由描述用户的信息进行驱动。
How is it used by the MVC framework?
如何在MVC框架中使用它?
This feature isn’t used directly by the MVC framework, but it is integrated into the standard authorization features, such as the Authorize attribute.
这不是直接由MVC框架使用的特性,但它集成到了标准的授权特性之中,例如Authorize注解属性。

Tip you don’t have to use claims in your applications, and as Chapter 14 showed, ASP.NET Identity is perfectly happy providing an application with the authentication and authorization services without any need to understand claims at all.
提示:你在应用程序中不一定要使用声明(Claims),正如第14章所展示的那样,ASP.NET Identity能够为应用程序提供充分的认证与授权服务,而根本不需要理解声明(Claims)。

15.3.1 Understanding Claims
15.3.1 理解声明(Claims)

A claim is a piece of information about the user, along with some information about where the information came from. The easiest way to unpack claims is through some practical demonstrations, without which any discussion becomes too abstract to be truly useful. To get started, I added a Claims controller to the example project, the definition of which you can see in Listing 15-12.
一项声明(Claim)是关于用户的一个信息片段(请注意这个英文单词的单复数形式——译者注),并伴有该片段出自何处的某种信息。揭开声明(Claims)含义最容易的方式是做一些实际演示,任何讨论都会过于抽象根本没有真正的用处。为此,我在示例项目中添加了一个Claims控制器,其定义如清单15-12所示。

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

using System.Security.Claims;
using System.Web;
using System.Web.Mvc; 
namespace Users.Controllers { public class ClaimsController : Controller {
[Authorize] public ActionResult Index() { ClaimsIdentity ident = HttpContext.User.Identity as ClaimsIdentity; if (ident == null) { return View("Error", new string[] { "No claims available" }); } else { return View(ident.Claims); } } } }

Tip You may feel a little lost as I define the code for this example. Don’t worry about the details for the moment—just stick with it until you see the output from the action method and view that I define. More than anything else, that will help put claims into perspective.
提示:你或许会对我为此例定义的代码感到有点失望。此刻对此细节不必着急——只要稍事忍耐,当看到该动作方法和视图的输出便会明白。尤为重要的是,这有助于洞察声明(Claims)。

You can get the claims associated with a user in different ways. One approach is to use the Claims property defined by the user class, but in this example, I have used the HttpContext.User.Identity property to demonstrate the way that ASP.NET Identity is integrated with the rest of the ASP.NET platform. As I explained in Chapter 13, the HttpContext.User.Identity property returns an implementation of the IIdentity interface, which is a ClaimsIdentity object when working using ASP.NET Identity. The ClaimsIdentity class is defined in the System.Security.Claims namespace, and Table 15-5 shows the members it defines that are relevant to this chapter.
可以通过不同的方式获得与用户相关联的声明(Claims)。方法之一就是使用由用户类定义的Claims属性,但在这个例子中,我使用了HttpContext.User.Identity属性,目的是演示ASP.NET Identity与ASP.NET平台集成的方式(请注意这句话所表示的含义:用户类的Claims属性属于ASP.NET Identity,而HttpContext.User.Identity属性则属于ASP.NET平台。由此可见,ASP.NET Identity已经融合到了ASP.NET平台之中——译者注)。正如第13章所解释的那样,HttpContext.User.Identity属性返回IIdentity的接口实现,当使用ASP.NET Identity时,该实现是一个ClaimsIdentity对象。ClaimsIdentity类是在System.Security.Claims命名空间中定义的,表15-5显示了它所定义的与本章有关的成员。

Table 15-5. The Members Defined by the ClaimsIdentity Class
表15-5. ClaimsIdentity类所定义的成员
Name
名称
Description
描述
Claims Returns an enumeration of Claim objects representing the claims for the user.
返回表示用户声明(Claims)的Claim对象枚举
AddClaim(claim) Adds a claim to the user identity.
给用户添加一个声明(Claim)
AddClaims(claims) Adds an enumeration of Claim objects to the user identity.
给用户添加Claim对象的枚举。
HasClaim(predicate) Returns true if the user identity contains a claim that matches the specified predicate. See the “Applying Claims” section for an example predicate.
如果用户含有与指定谓词匹配的声明(Claim)时,返回true。参见“运用声明(Claims)”中的示例谓词
RemoveClaim(claim) Removes a claim from the user identity.
删除用户的声明(Claim)。

Other members are available, but the ones in the table are those that are used most often in web applications, for reason that will become obvious as I demonstrate how claims fit into the wider ASP.NET platform.
还有一些可用的其它成员,但表中的这些是在Web应用程序中最常用的,随着我演示如何将声明(Claims)融入更宽泛的ASP.NET平台,它们为什么最常用就很显然了。

In Listing 15-12, I cast the IIdentity implementation to the ClaimsIdentity type and pass the enumeration of Claim objects returned by the ClaimsIdentity.Claims property to the View method. A Claim object represents a single piece of data about the user, and the Claim class defines the properties shown in Table 15-6.
在清单15-12中,我将IIdentity实现转换成了ClaimsIdentity类型,并且给View方法传递了ClaimsIdentity.Claims属性所返回的Claim对象的枚举。Claim对象所示表示的是关于用户的一个单一的数据片段,Claim类定义的属性如表15-6所示。

Table 15-6. The Properties Defined by the Claim Class
表15-6. Claim类定义的属性
Name
名称
Description
描述
Issuer Returns the name of the system that provided the claim
返回提供声明(Claim)的系统名称
Subject Returns the ClaimsIdentity object for the user who the claim refers to
返回声明(Claim)所指用户的ClaimsIdentity对象
Type Returns the type of information that the claim represents
返回声明(Claim)所表示的信息类型
Value Returns the piece of information that the claim represents
返回声明(Claim)所表示的信息片段

Listing 15-13 shows the contents of the Index.cshtml file that I created in the Views/Claims folder and that is rendered by the Index action of the Claims controller. The view adds a row to a table for each claim about the user.
清单15-13显示了我在Views/Claims文件夹中创建的Index.cshtml文件的内容,它由Claims控制器中的Index动作方法进行渲染。该视图为用户的每项声明(Claim)添加了一个表格行。

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

@using System.Security.Claims
@using Users.Infrastructure
@model IEnumerable<Claim>
@{ ViewBag.Title = "Claims"; }
<div class="panel panel-primary"> <div class="panel-heading"> Claims </div> <table class="table table-striped"> <tr> <th>Subject</th><th>Issuer</th> <th>Type</th><th>Value</th> </tr> @foreach (Claim claim in Model.OrderBy(x => x.Type)) { <tr> <td>@claim.Subject.Name</td> <td>@claim.Issuer</td> <td>@Html.ClaimType(claim.Type)</td> <td>@claim.Value</td> </tr> } </table> </div>

The value of the Claim.Type property is a URI for a Microsoft schema, which isn’t especially useful. The popular schemas are used as the values for fields in the System.Security.Claims.ClaimTypes class, so to make the output from the Index.cshtml view easier to read, I added an HTML helper to the IdentityHelpers.cs file, as shown in Listing 15-14. It is this helper that I use in the Index.cshtml file to format the value of the Claim.Type property.
Claim.Type属性的值是一个微软模式(Microsoft Schema)的URI(统一资源标识符),这是特别有用的。System.Security.Claims.ClaimTypes类中字段的值使用的是流行模式(Popular Schema),因此为了使Index.cshtml视图的输出更易于阅读,我在IdentityHelpers.cs文件中添加了一个HTML辅助器,如清单15-14所示。Index.cshtml文件正是使用这个辅助器格式化了Claim.Type属性的值。

Listing 15-14. Adding a Helper to the IdentityHelpers.cs File
清单15-14. 在IdentityHelpers.cs文件中添加辅助器

using System.Web;
using System.Web.Mvc;
using Microsoft.AspNet.Identity.Owin;
using System;
using System.Linq;
using System.Reflection;
using System.Security.Claims;
namespace Users.Infrastructure { public static class IdentityHelpers {
public static MvcHtmlString GetUserName(this HtmlHelper html, string id) { AppUserManager mgr = HttpContext.Current.GetOwinContext().GetUserManager<AppUserManager>(); return new MvcHtmlString(mgr.FindByIdAsync(id).Result.UserName); }
public static MvcHtmlString ClaimType(this HtmlHelper html, string claimType) { FieldInfo[] fields = typeof(ClaimTypes).GetFields(); foreach (FieldInfo field in fields) { if (field.GetValue(null).ToString() == claimType) { return new MvcHtmlString(field.Name); } } return new MvcHtmlString(string.Format("{0}", claimType.Split(‘/‘, ‘.‘).Last())); } } }

Note The helper method isn’t at all efficient because it reflects on the fields of the ClaimType class for each claim that is displayed, but it is sufficient for my purposes in this chapter. You won’t often need to display the claim type in real applications.
注:该辅助器并非十分有效,因为它只是针对每个要显示的声明(Claim)映射出ClaimType类的字段,但对我要的目的已经足够了。在实际项目中不会经常需要显示声明(Claim)的类型。

To see why I have created a controller that uses claims without real

以上是关于ASP.NET Identity系列教程Identity高级技术的主要内容,如果未能解决你的问题,请参考以下文章

ASP.NET Identity系列教程(目录)

ASP.NET Identity系列教程

ASP.NET Identity教程ASP.NET Identity入门

第16章 使用ASP.NET Core Identity

asp.net core系列 48 Identity 身份模型自定义

ASP.NET Core Identity 系列之四