【ASP.NET Identity系列教程(三)】Identity高级技术

时间:2021-01-07 20:08:42

注:本文是【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显示了数据库的状态,它含有上一章的用户Admin、Alice、Bob以及Joe。为了检查用户,请启动应用程序,请求/Admin/Index URL,并以Admin用户进行认证。

【ASP.NET Identity系列教程(三)】Identity高级技术

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控制器创建了角色Users和Employees,并为这些角色指定了一些用户,如表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控制器所显示出来的必要的角色配置。

【ASP.NET Identity系列教程(三)】Identity高级技术

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 = "admin@example.com";
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所示。

【ASP.NET Identity系列教程(三)】Identity高级技术

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属性,但只当Country为NONE时才进行设置(在迁移数据库时,所有用户都是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 = "admin@example.com";
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所示。

【ASP.NET Identity系列教程(三)】Identity高级技术

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 really explaining what they are, start the application, authenticate as the user Alice (with the password MySecret), and request the /Claims/Index URL. Figure 15-5 shows the content that is generated.
为了弄明白我为何要先创建一个使用声明(Claims)的控制器,而没有真正解释声明(Claims)是什么的原因,可以启动应用程序,以用户Alice进行认证(其口令是MySecret),并请求/Claims/Index URL。图15-5显示了生成的内容。

【ASP.NET Identity系列教程(三)】Identity高级技术

Figure 15-5. The output from the Index action of the Claims controller
图15-5. Claims控制器中Index动作的输出

It can be hard to make out the detail in the figure, so I have reproduced the content in Table 15-7.
这可能还难以认识到此图的细节,为此我在表15-7中重列了其内容。

Table 15-7. The Data Shown in Figure 15-5
表15-7. 图15-5中显示的数据
Subject(科目) Issuer(发行者) Type(类型) Value(值)
Alice LOCAL AUTHORITY SecurityStamp Unique ID
Alice LOCAL AUTHORITY IdentityProvider ASP.NET Identity
Alice LOCAL AUTHORITY Role Employees
Alice LOCAL AUTHORITY Role Users
Alice LOCAL AUTHORITY Name Alice
Alice LOCAL AUTHORITY NameIdentifier Alice’s user ID

The table shows the most important aspect of claims, which is that I have already been using them when I implemented the traditional authentication and authorization features in Chapter 14. You can see that some of the claims relate to user identity (the Name claim is Alice, and the NameIdentifier claim is Alice’s unique user ID in my ASP.NET Identity database).
此表展示了声明(Claims)最重要的方面,这些是我在第14章中实现传统的认证和授权特性时,一直在使用的信息。可以看出,有些声明(Claims)与用户标识有关(Name声明是Alice,NameIdentifier声明是Alice在ASP.NET Identity数据库中的唯一用户ID号)。

Other claims show membership of roles—there are two Role claims in the table, reflecting the fact that Alice is assigned to both the Users and Employees roles. There is also a claim about how Alice has been authenticated: The IdentityProvider is set to ASP.NET Identity.
其他声明(Claims)显示了角色成员——表中有两个Role声明(Claim),体现出Alice被赋予了Users和Employees两个角色这一事实。还有一个是Alice已被认证的声明(Claim):IdentityProvider被设置到了ASP.NET Identity。

The difference when this information is expressed as a set of claims is that you can determine where the data came from. The Issuer property for all the claims shown in the table is set to LOCAL AUTHORITY, which indicates that the user’s identity has been established by the application.
当这种信息被表示成一组声明(Claims)时的差别是,你能够确定这些数据是从哪里来的。表中所显示的所有声明的Issuer属性(发布者)都被设置到了LOACL AUTHORITY(本地授权),这说明该用户的标识是由应用程序建立的。

So, now that you have seen some example claims, I can more easily describe what a claim is. A claim is any piece of information about a user that is available to the application, including the user’s identity and role memberships. And, as you have seen, the information I have been defining about my users in earlier chapters is automatically made available as claims by ASP.NET Identity.
因此,现在你已经看到了一些声明(Claims)示例,我可以更容易地描述声明(Claim)是什么了。一项声明(Claim)是可用于应用程序中的有关用户的一个信息片段,包括用户的标识以及角色成员等。而且,正如你所看到的,我在前几章定义的关于用户的信息,被ASP.NET Identity自动地作为声明(Claims)了。

15.3.2 Creating and Using Claims
15.3.2 创建和使用声明(Claims)

Claims are interesting for two reasons. The first reason is that an application can obtain claims from multiple sources, rather than just relying on a local database for information about the user. You will see a real example of this when I show you how to authenticate users through a third-party system in the “Using Third-Party Authentication” section, but for the moment I am going to add a class to the example project that simulates a system that provides claims information. Listing 15-15 shows the contents of the LocationClaimsProvider.cs file that I added to the Infrastructure folder.
声明(Claims)比较有意思的原因有两个。第一个原因是应用程序可以从多个来源获取声明(Claims),而不是只能依靠本地数据库关于用户的信息。你将会看到一个实际的示例,在“使用第三方认证”小节中,将演示如何通过第三方系统来认证用户。不过,此刻我只打算在示例项目中添加一个类,用以模拟一个提供声明(Claims)信息的系统。清单15-15显示了我添加到Infrastructure文件夹中LocationClaimsProvider.cs文件的内容。

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

using System.Collections.Generic;
using System.Security.Claims;
namespace Users.Infrastructure {
public static class LocationClaimsProvider {
public static IEnumerable<Claim> GetClaims(ClaimsIdentity user) {
List<Claim> claims = new List<Claim>();
if (user.Name.ToLower() == "alice") {
claims.Add(CreateClaim(ClaimTypes.PostalCode, "DC 20500"));
claims.Add(CreateClaim(ClaimTypes.StateOrProvince, "DC"));
} else {
claims.Add(CreateClaim(ClaimTypes.PostalCode, "NY 10036"));
claims.Add(CreateClaim(ClaimTypes.StateOrProvince, "NY"));
}
return claims;
}
private static Claim CreateClaim(string type, string value) {
return new Claim(type, value, ClaimValueTypes.String, "RemoteClaims");
}
}
}

The GetClaims method takes a ClaimsIdentity argument and uses the Name property to create claims about the user’s ZIP code and state. This class allows me to simulate a system such as a central HR database, which would be the authoritative source of location information about staff, for example.
GetClaims方法以ClaimsIdentity为参数,并使用Name属性创建了关于用户ZIP码(邮政编码)和州府的声明(Claims)。上述这个类使我能够模拟一个诸如中心化的HR数据库(人力资源数据库)之类的系统,它可能会成为全体职员的地点信息的权威数据源。

Claims are associated with the user’s identity during the authentication process, and Listing 15-16 shows the changes I made to the Login action method of the Account controller to call the LocationClaimsProvider class.
在认证过程期间,声明(Claims)是与用户标识关联在一起的,清单15-16显示了我对Account控制器中Login动作方法所做的修改,以便调用LocationClaimsProvider类。

Listing 15-16. Associating Claims with a User in the AccountController.cs File
清单15-16. AccountController.cs文件中用户用声明的关联

...
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginModel details, string returnUrl) {
if (ModelState.IsValid) {
AppUser user = await UserManager.FindAsync(details.Name,
details.Password);
if (user == null) {
ModelState.AddModelError("", "Invalid name or password.");
} else {
ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,
DefaultAuthenticationTypes.ApplicationCookie);
ident.AddClaims(LocationClaimsProvider.GetClaims(ident));
AuthManager.SignOut();
AuthManager.SignIn(new AuthenticationProperties {
IsPersistent = false
}, ident);
return Redirect(returnUrl);
}
}
ViewBag.returnUrl = returnUrl;
return View(details);
}
...

You can see the effect of the location claims by starting the application, authenticating as a user, and requesting the /Claim/Index URL. Figure 15-6 shows the claims for Alice. You may have to sign out and sign back in again to see the change.
为了看看这个地点声明(Claims)的效果,可以启动应用程序,以一个用户进行认证,并请求/Claim/Index URL。图15-6显示了Alice的声明(Claims)。你可能需要退出,然后再次登录才会看到发生的变化。

【ASP.NET Identity系列教程(三)】Identity高级技术

Figure 15-6. Defining additional claims for users
图15-6. 定义用户的附加声明

Obtaining claims from multiple locations means that the application doesn’t have to duplicate data that is held elsewhere and allows integration of data from external parties. The Claim.Issuer property tells you where a claim originated from, which helps you judge how accurate the data is likely to be and how much weight you should give the data in your application. Location data obtained from a central HR database is likely to be more accurate and trustworthy than data obtained from an external mailing list provider, for example.
从多个地点获取声明(Claims)意味着应用程序不必复制其他地方保持的数据,并且能够与外部的数据集成。Claim.Issuer属性(图15-6中的Issuer数据列——译者注)能够告诉你一个声明(Claim)的发源地,这有助于让你判断数据的精确程度,也有助于让你决定这类数据在应用程序中的权重。例如,从中心化的HR数据库获取的地点数据可能要比外部邮件列表提供器获取的数据更为精确和可信。

1. Applying Claims
1. 运用声明(Claims)

The second reason that claims are interesting is that you can use them to manage user access to your application more flexibly than with standard roles. The problem with roles is that they are static, and once a user has been assigned to a role, the user remains a member until explicitly removed. This is, for example, how long-term employees of big corporations end up with incredible access to internal systems: They are assigned the roles they require for each new job they get, but the old roles are rarely removed. (The unexpectedly broad systems access sometimes becomes apparent during the investigation into how someone was able to ship the contents of the warehouse to their home address—true story.)
声明(Claims)有意思的第二个原因是,你可以用它们来管理用户对应用程序的访问,这要比标准的角色管理更为灵活。角色的问题在于它们是静态的,而且一旦用户已经被赋予了一个角色,该用户便是一个成员,直到明确地删除为止。例如,这意味着大公司的长期雇员,对内部系统的访问会十分惊人:他们每次在获得新工作时,都会赋予所需的角色,但旧角色很少被删除。(在调查某人为何能够将仓库里的东西发往他的家庭地址过程中发现,有时会出现异常宽泛的系统访问——真实的故事)

Claims can be used to authorize users based directly on the information that is known about them, which ensures that the authorization changes when the data changes. The simplest way to do this is to generate Role claims based on user data that are then used by controllers to restrict access to action methods. Listing 15-17 shows the contents of the ClaimsRoles.cs file that I added to the Infrastructure.
声明(Claims)可以直接根据用户已知的信息对用户进行授权,这能够保证当数据发生变化时,授权也随之而变。此事最简单的做法是根据用户数据来生成Role声明(Claim),然后由控制器用来限制对动作方法的访问。清单15-17显示了我添加到Infrastructure中的ClaimsRoles.cs文件的内容。

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

using System.Collections.Generic;
using System.Security.Claims;
namespace Users.Infrastructure {
public class ClaimsRoles {
public static IEnumerable<Claim> CreateRolesFromClaims(ClaimsIdentity user) {
List<Claim> claims = new List<Claim>();
if (user.HasClaim(x => x.Type == ClaimTypes.StateOrProvince
&& x.Issuer == "RemoteClaims" && x.Value == "DC")
&& user.HasClaim(x => x.Type == ClaimTypes.Role
&& x.Value == "Employees")) {
claims.Add(new Claim(ClaimTypes.Role, "DCStaff"));
}
return claims;
}
}
}

The gnarly looking CreateRolesFromClaims method uses lambda expressions to determine whether the user has a StateOrProvince claim from the RemoteClaims issuer with a value of DC and a Role claim with a value of Employees. If the user has both claims, then a Role claim is returned for the DCStaff role. Listing 15-18 shows how I call the CreateRolesFromClaims method from the Login action in the Account controller.
CreateRolesFromClaims是一个粗糙的考察方法,它使用了Lambda表达式,以检查用户是否具有StateOrProvince声明(Claim),该声明来自于RemoteClaims发行者(Issuer),值为DC。也检查用户是否具有Role声明(Claim),其值为Employees。如果用户这两个声明都有,那么便返回一个DCStaff角色的Role声明。清单15-18显示了如何在Account控制器中的Login动作中调用CreateRolesFromClaims方法。

Listing 15-18. Generating Roles Based on Claims in the AccountController.cs File
清单15-18. 在AccountController.cs中根据声明生成角色

...
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginModel details, string returnUrl) {
if (ModelState.IsValid) {
AppUser user = await UserManager.FindAsync(details.Name,
details.Password);
if (user == null) {
ModelState.AddModelError("", "Invalid name or password.");
} else {
ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,
DefaultAuthenticationTypes.ApplicationCookie);
ident.AddClaims(LocationClaimsProvider.GetClaims(ident));
ident.AddClaims(ClaimsRoles.CreateRolesFromClaims(ident));
AuthManager.SignOut();
AuthManager.SignIn(new AuthenticationProperties {
IsPersistent = false
}, ident);
return Redirect(returnUrl);
}
}
ViewBag.returnUrl = returnUrl;
return View(details);
}
...

I can then restrict access to an action method based on membership of the DCStaff role. Listing 15-19 shows a new action method I added to the Claims controller to which I have applied the Authorize attribute.
然后我可以根据DCStaff角色的成员,来限制对一个动作方法的访问。清单15-19显示了在Claims控制器中添加的一个新的动作方法,在该方法上已经运用了Authorize注解属性。

Listing 15-19. Adding a New Action Method to the ClaimsController.cs File
清单15-19. 在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);
}
}
[Authorize(Roles="DCStaff")]
public string OtherAction() {
return "This is the protected action";
}

}
}

Users will be able to access OtherAction only if their claims grant them membership to the DCStaff role. Membership of this role is generated dynamically, so a change to the user’s employment status or location information will change their authorization level.
只要用户的声明(Claims)承认他们是DCStaff角色的成员,那么他们便能访问OtherAction动作。该角色的成员是动态生成的,因此,若是用户的雇用状态或地点信息发生变化,也会改变他们的授权等级。

提示:请读者从这个例子中吸取其中的思想精髓。对于读物的理解程度,仁者见仁,智者见智,能领悟多少,全凭各人,译者感觉这里的思想有无数的可能。举例说明:(1)可以根据用户的身份进行授权,比如学生在校时是“学生”,毕业后便是“校友”;(2)可以根据用户所处的部门进行授权,人事部用户属于人事团队,销售部用户属于销售团队,各团队有其自己的应用;(3)下一小节的示例是根据用户的地点授权。简言之:一方面用户的各种声明(Claim)都可以用来进行授权;另一方面用户的声明(Claim)又是可以自定义的。于是可能的运用就无法估计了。总之一句话,这种基于声明的授权(Claims-Based Authorization)有无限可能!要是没有我这里的提示,是否所有读者在此处都会有所体会?——译者注

15.3.3 Authorizing Access Using Claims
15.3.3 使用声明(Claims)授权访问

The previous example is an effective demonstration of how claims can be used to keep authorizations fresh and accurate, but it is a little indirect because I generate roles based on claims data and then enforce my authorization policy based on the membership of that role. A more direct and flexible approach is to enforce authorization directly by creating a custom authorization filter attribute. Listing 15-20 shows the contents of the ClaimsAccessAttribute.cs file, which I added to the Infrastructure folder and used to create such a filter.
前面的示例有效地演示了如何用声明(Claims)来保持新鲜和准确的授权,但有点不太直接,因为我要根据声明(Claims)数据来生成了角色,然后强制我的授权策略基于角色成员。一个更直接且灵活的办法是直接强制授权,其做法是创建一个自定义的授权过滤器注解属性。清单15-20演示了ClaimsAccessAttribute.cs文件的内容,我将它添加在Infrastructure文件夹中,并用它创建了这种过滤器。

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

using System.Security.Claims;
using System.Web;
using System.Web.Mvc;
namespace Users.Infrastructure {
public class ClaimsAccessAttribute : AuthorizeAttribute {
public string Issuer { get; set; }
public string ClaimType { get; set; }
public string Value { get; set; }
protected override bool AuthorizeCore(HttpContextBase context) {
return context.User.Identity.IsAuthenticated
&& context.User.Identity is ClaimsIdentity
&& ((ClaimsIdentity)context.User.Identity).HasClaim(x =>
x.Issuer == Issuer && x.Type == ClaimType && x.Value == Value
);
}
}
}

The attribute I have defined is derived from the AuthorizeAttribute class, which makes it easy to create custom authorization policies in MVC framework applications by overriding the AuthorizeCore method. My implementation grants access if the user is authenticated, the IIdentity implementation is an instance of ClaimsIdentity, and the user has a claim with the issuer, type, and value matching the class properties. Listing 15-21 shows how I applied the attribute to the Claims controller to authorize access to the OtherAction method based on one of the location claims created by the LocationClaimsProvider class.
我所定义的这个注解属性派生于AuthorizeAttribute类,通过重写AuthorizeCore方法,很容易在MVC框架应用程序中创建自定义的授权策略。在这个实现中,若用户是已认证的、其IIdentity实现是一个ClaimsIdentity实例,而且该用户有一个带有issuer、type以及value的声明(Claim),它们与这个类的属性是匹配的,则该用户便是允许访问的。清单15-21显示了如何将这个注解属性运用于Claims控制器,以便根据LocationClaimsProvider类创建的地点声明(Claim),对OtherAction方法进行授权访问。

Listing 15-21. Performing Authorization on Claims in the ClaimsController.cs File
清单15-21. 在ClaimsController.cs文件中执行基于声明的授权

using System.Security.Claims;
using System.Web;
using System.Web.Mvc;
using Users.Infrastructure;
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);
}
}
[ClaimsAccess(Issuer="RemoteClaims", ClaimType=ClaimTypes.PostalCode,
Value="DC 20500")]

public string OtherAction() {
return "This is the protected action";
}
}
}

My authorization filter ensures that only users whose location claims specify a ZIP code of DC 20500 can invoke the OtherAction method.
这个授权过滤器能够确保只有地点声明(Claim)的邮编为DC 20500的用户才能请求OtherAction方法。

15.4 Using Third-Party Authentication
15.4 使用第三方认证

One of the benefits of a claims-based system such as ASP.NET Identity is that any of the claims can come from an external system, even those that identify the user to the application. This means that other systems can authenticate users on behalf of the application, and ASP.NET Identity builds on this idea to make it simple and easy to add support for authenticating users through third parties such as Microsoft, Google, Facebook, and Twitter.
基于声明的系统,如ASP.NET Identity,的好处之一是任何声明都可以来自于外部系统,即使是将用户标识到应用程序的那些声明。这意味着其他系统可以代表应用程序来认证用户,而ASP.NET Identity就建立在这样的思想之上,使之能够简单而方便地添加第三方认证用户的支持,如微软、Google、Facebook、Twitter等。

There are some substantial benefits of using third-party authentication: Many users will already have an account, users can elect to use two-factor authentication, and you don’t have to manage user credentials in the application. In the sections that follow, I’ll show you how to set up and use third-party authentication for Google users, which Table 15-8 puts into context.
使用第三方认证有一些实际的好处:许多用户已经有了账号、用户可以选择使用双因子认证、你不必在应用程序中管理用户凭据等等。在以下小节中,我将演示如何为Google用户建立并使用第三方认证,表15-8描述了事情的情形。

Table 15-8. Putting Third-Party Authentication in Context
表15-8. 第三方认证情形
Question
问题
Answer
回答
What is it?
什么是第三方认证?
Authenticating with third parties lets you take advantage of the popularity of companies such as Google and Facebook.
第三方认证使你能够利用流行公司,如Google和Facebook,的优势。
Why should I care?
为何要关心它?
Users don’t like having to remember passwords for many different sites. Using a provider with large-scale adoption can make your application more appealing to users of the provider’s services.
用户不喜欢记住许多不同网站的口令。使用大范围适应的提供器可使你的应用程序更吸引有提供器服务的用户。
How is it used by the MVC framework?
如何在MVC框架中使用它?
This feature isn’t used directly by the MVC framework.
这不是一个直接由MVC框架使用的特性。

Note The reason I have chosen to demonstrate Google authentication is that it is the only option that doesn’t require me to register my application with the authentication service. You can get details of the registration processes required at http://bit.ly/1cqLTrE.
提示:我选择演示Google认证的原因是,它是唯一不需要在其认证服务中注册我应用程序的公司。有关认证服务注册过程的细节,请参阅http://bit.ly/1cqLTrE。

15.4.1 Enabling Google Authentication
15.4.1 启用Google认证

ASP.NET Identity comes with built-in support for authenticating users through their Microsoft, Google, Facebook, and Twitter accounts as well more general support for any authentication service that supports OAuth. The first step is to add the NuGet package that includes the Google-specific additions for ASP.NET Identity. Enter the following command into the Package Manager Console:
ASP.NET Identity带有通过Microsoft、Google、Facebook以及Twitter账号认证用户的内建支持,并且对于支持OAuth的认证服务具有更普遍的支持。第一个步骤是添加NuGet包,包中含有用于ASP.NET Identity的Google专用附件。请在“Package Manager Console(包管理器控制台)”中输入以下命令:

Install-Package Microsoft.Owin.Security.Google -version 2.0.2

There are NuGet packages for each of the services that ASP.NET Identity supports, as described in Table 15-9.
对于ASP.NET Identity支持的每一种服务都有相应的NuGet包,如表15-9所示。

Table 15-9. The NuGet Authenticaton Packages
表15-9. NuGet认证包
Name
名称
Description
描述
Microsoft.Owin.Security.Google Authenticates users with Google accounts
用Google账号认证用户
Microsoft.Owin.Security.Facebook Authenticates users with Facebook accounts
用Facebook账号认证用户
Microsoft.Owin.Security.Twitter Authenticates users with Twitter accounts
用Twitter账号认证用户
Microsoft.Owin.Security.MicrosoftAccount Authenticates users with Microsoft accounts
用Microsoft账号认证用户
Microsoft.Owin.Security.OAuth Authenticates users against any OAuth 2.0 service
根据任一OAuth 2.0服务认证用户

Once the package is installed, I enable support for the authentication service in the OWIN startup class, which is defined in the App_Start/IdentityConfig.cs file in the example project. Listing 15-22 shows the change that I have made.
一旦安装了这个包,便可以在OWIN启动类中启用此项认证服务的支持,启动类的定义在示例项目的App_Start/IdentityConfig.cs文件中。清单15-22显示了所做的修改。

Listing 15-22. Enabling Google Authentication in the IdentityConfig.cs File
清单15-22. 在IdentityConfig.cs文件中启用Google认证

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

}
}
}

Each of the packages that I listed in Table 15-9 contains an extension method that enables the corresponding service. The extension method for the Google service is called UseGoogleAuthentication, and it is called on the IAppBuilder implementation that is passed to the Configuration method.
表15-9所列的每个包都含有启用相应服务的扩展方法。用于Google服务的扩展方法名称为UseGoogleAuthentication,它通过传递给Configuration方法的IAppBuilder实现进行调用。

Next I added a button to the Views/Account/Login.cshtml file, which allows users to log in via Google. You can see the change in Listing 15-23.
下一步骤是在Views/Account/Login.cshtml文件中添加一个按钮,让用户能够通过Google进行登录。所做的修改如清单15-23所示。

Listing 15-23. Adding a Google Login Button to the Login.cshtml File
清单15-23. 在Login.cshtml文件中添加Google登录按钮

@model Users.Models.LoginModel
@{ ViewBag.Title = "Login";}
<h2>Log In</h2>
@Html.ValidationSummary()
@using (Html.BeginForm()) {
@Html.AntiForgeryToken();
<input type="hidden" name="returnUrl" value="@ViewBag.returnUrl" />
<div class="form-group">
<label>Name</label>
@Html.TextBoxFor(x => x.Name, new { @class = "form-control" })
</div>
<div class="form-group">
<label>Password</label>
@Html.PasswordFor(x => x.Password, new { @class = "form-control" })
</div>
<button class="btn btn-primary" type="submit">Log In</button>
}
@using (Html.BeginForm("GoogleLogin", "Account")) {
<input type="hidden" name="returnUrl" value="@ViewBag.returnUrl" />
<button class="btn btn-primary" type="submit">Log In via Google</button>

}

The new button submits a form that targets the GoogleLogin action on the Account controller. You can see this method—and the other changes I made the controller—in Listing 15-24.
新按钮递交一个表单,目标是Account控制器中的GoogleLogin动作。可从清单15-24中看到该方法,以及在控制器中所做的其他修改。

Listing 15-24. Adding Support for Google Authentication to the AccountController.cs File
清单15-24. 在AccountController.cs文件中添加Google认证支持

using System.Threading.Tasks;
using System.Web.Mvc;
using Users.Models;
using Microsoft.Owin.Security;
using System.Security.Claims;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Users.Infrastructure;
using System.Web;
namespace Users.Controllers {
[Authorize]
public class AccountController : Controller {
[AllowAnonymous]
public ActionResult Login(string returnUrl) {
if (HttpContext.User.Identity.IsAuthenticated) {
return View("Error", new string[] { "Access Denied" });
}
ViewBag.returnUrl = returnUrl;
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginModel details, string returnUrl) {
if (ModelState.IsValid) {
AppUser user = await UserManager.FindAsync(details.Name,
details.Password);
if (user == null) {
ModelState.AddModelError("", "Invalid name or password.");
} else {
ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,
DefaultAuthenticationTypes.ApplicationCookie);
ident.AddClaims(LocationClaimsProvider.GetClaims(ident));
ident.AddClaims(ClaimsRoles.CreateRolesFromClaims(ident));
AuthManager.SignOut();
AuthManager.SignIn(new AuthenticationProperties {
IsPersistent = false
}, ident);
return Redirect(returnUrl);
}
}
ViewBag.returnUrl = returnUrl;
return View(details);
}
[HttpPost]
[AllowAnonymous]
public ActionResult GoogleLogin(string returnUrl) {
var properties = new AuthenticationProperties {
RedirectUri = Url.Action("GoogleLoginCallback",
new { returnUrl = returnUrl})
};
HttpContext.GetOwinContext().Authentication.Challenge(properties, "Google");
return new HttpUnauthorizedResult();
}
[AllowAnonymous]
public async Task<ActionResult> GoogleLoginCallback(string returnUrl) {
ExternalLoginInfo loginInfo = await AuthManager.GetExternalLoginInfoAsync();
AppUser user = await UserManager.FindAsync(loginInfo.Login);
if (user == null) {
user = new AppUser {
Email = loginInfo.Email,
UserName = loginInfo.DefaultUserName,
City = Cities.LONDON, Country = Countries.UK
};
IdentityResult result = await UserManager.CreateAsync(user);
if (!result.Succeeded) {
return View("Error", result.Errors);
} else {
result = await UserManager.AddLoginAsync(user.Id, loginInfo.Login);
if (!result.Succeeded) {
return View("Error", result.Errors);
}
}
}
ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,
DefaultAuthenticationTypes.ApplicationCookie);
ident.AddClaims(loginInfo.ExternalIdentity.Claims);
AuthManager.SignIn(new AuthenticationProperties {
IsPersistent = false }, ident);
return Redirect(returnUrl ?? "/");
}

[Authorize]
public ActionResult Logout() {
AuthManager.SignOut();
return RedirectToAction("Index", "Home");
}
private IAuthenticationManager AuthManager {
get {
return HttpContext.GetOwinContext().Authentication;
}
}
private AppUserManager UserManager {
get {
return HttpContext.GetOwinContext().GetUserManager<AppUserManager>();
}
}
}
}

The GoogleLogin method creates an instance of the AuthenticationProperties class and sets the RedirectUri property to a URL that targets the GoogleLoginCallback action in the same controller. The next part is a magic phrase that causes ASP.NET Identity to respond to an unauthorized error by redirecting the user to the Google authentication page, rather than the one defined by the application:
GoogleLogin方法创建了AuthenticationProperties类的一个实例,并为RedirectUri属性设置了一个URL,其目标为同一控制器中的GoogleLoginCallback动作。下一个部分是一个神奇阶段,通过将用户重定向到Google认证页面,而不是应用程序所定义的认证页面,让ASP.NET Identity对未授权的错误进行响应:

...
HttpContext.GetOwinContext().Authentication.Challenge(properties, "Google");
return new HttpUnauthorizedResult();
...

This means that when the user clicks the Log In via Google button, their browser is redirected to the Google authentication service and then redirected back to the GoogleLoginCallback action method once they are authenticated.
这意味着,当用户通过点击Google按钮进行登录时,浏览器被重定向到Google的认证服务,一旦在那里认证之后,便被重定向回GoogleLoginCallback动作方法。

I get details of the external login by calling the GetExternalLoginInfoAsync of the IAuthenticationManager implementation, like this:
我通过调用IAuthenticationManager实现的GetExternalLoginInfoAsync方法,我获得了外部登录的细节,如下所示:

...
ExternalLoginInfo loginInfo = await AuthManager.GetExternalLoginInfoAsync();
...

The ExternalLoginInfo class defines the properties shown in Table 15-10.
ExternalLoginInfo类定义的属性如表15-10所示:

Table 15-10. The Properties Defined by the ExternalLoginInfo Class
表15-10. ExternalLoginInfo类所定义的属性
Name
名称
Description
描述
DefaultUserName Returns the username
返回用户名
Email Returns the e-mail address
返回E-mail地址
ExternalIdentity Returns a ClaimsIdentity that identities the user
返回标识该用户的ClaimsIdentity
Login Returns a UserLoginInfo that describes the external login
返回描述外部登录的UserLoginInfo

I use the FindAsync method defined by the user manager class to locate the user based on the value of the ExternalLoginInfo.Login property, which returns an AppUser object if the user has been authenticated with the application before:
我使用了由用户管理器类所定义的FindAsync方法,以便根据ExternalLoginInfo.Login属性的值对用户进行定位,如果用户之前在应用程序中已经认证,该属性会返回一个AppUser对象:

...
AppUser user = await UserManager.FindAsync(loginInfo.Login);
...

If the FindAsync method doesn’t return an AppUser object, then I know that this is the first time that this user has logged into the application, so I create a new AppUser object, populate it with values, and save it to the database. I also save details of how the user logged in so that I can find them next time:
如果FindAsync方法返回的不是AppUser对象,那么我便知道这是用户首次登录应用程序,于是便创建了一个新的AppUser对象,填充该对象的值,并将其保存到数据库。我还保存了用户如何登录的细节,以便下次能够找到他们:

...
result = await UserManager.AddLoginAsync(user.Id, loginInfo.Login);
...

All that remains is to generate an identity the user, copy the claims provided by Google, and create an authentication cookie so that the application knows the user has been authenticated:
剩下的事情只是生成该用户的标识了,拷贝Google提供的声明(Claims),并创建一个认证Cookie,以使应用程序知道此用户已认证:

...
ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,
DefaultAuthenticationTypes.ApplicationCookie);
ident.AddClaims(loginInfo.ExternalIdentity.Claims);
AuthManager.SignIn(new AuthenticationProperties { IsPersistent = false }, ident);
...

15.4.2 Testing Google Authentication
15.4.2 测试Google认证

There is one further change that I need to make before I can test Google authentication: I need to change the account verification I set up in Chapter 13 because it prevents accounts from being created with e-mail addresses that are not within the example.com domain. Listing 15-25 shows how I removed the verification from the AppUserManager class.
在测试Google认证之前还需要一处修改:需要修改第13章所建立的账号验证,因为它不允许example.com域之外的E-mail地址创建账号。清单15-25显示了如何在AppUserManager类中删除这种验证。

Listing 15-25. Disabling Account Validation in the AppUserManager.cs File
清单15-25. 在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;
}
}
}

Tip you can use validation for externally authenticated accounts, but I am just going to disable the feature for simplicity.
提示:也可以使用外部已认证账号的验证,但这里出于简化,取消了这一特性。

To test authentication, start the application, click the Log In via Google button, and provide the credentials for a valid Google account. When you have completed the authentication process, your browser will be redirected back to the application. If you navigate to the /Claims/Index URL, you will be able to see how claims from the Google system have been added to the user’s identity, as shown in Figure 15-7.
为了测试认证,启动应用程序,通过点击“Log In via Google(通过Google登录)”按钮,并提供有效的Google账号凭据。当你完成了认证过程时,浏览器将被重定向回应用程序。如果导航到/Claims/Index URL,便能够看到来自Google系统的声明(Claims),已被添加到用户的标识中了,如图15-7所示。

【ASP.NET Identity系列教程(三)】Identity高级技术

Figure 15-7. Claims from Google
图15-7. 来自Google的声明(Claims)

15.5 Summary
15.5 小结

In this chapter, I showed you some of the advanced features that ASP.NET Identity supports. I demonstrated the use of custom user properties and how to use database migrations to preserve data when you upgrade the schema to support them. I explained how claims work and how they can be used to create more flexible ways of authorizing users. I finished the chapter by showing you how to authenticate users via Google, which builds on the ideas behind the use of claims.
本章向你演示了ASP.NET Identity所支持的一些高级特性。演示了自定义用户属性的使用,还演示了在升级数据架构时,如何使用数据库迁移保护数据。我解释了声明(Claims)的工作机制,以及如何将它们用于创建更灵活的用户授权方式。最后演示了如何通过Google进行认证结束了本章,这是建立在使用声明(Claims)的思想基础之上的。

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