rexian

咨询电话:023-6276-4481

热门文章

联系方式

电 话:023-6276-4481

邮箱:broiling@qq.com

地址:重庆市南岸区亚太商谷6幢25-2

当前位置:网站首页 > 技术文章 > ASP.NET Identity高级技术

ASP.NET Identity高级技术

编辑:T.T 发表时间:2017-07-26 08:55:26
T.T

本章将完成对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是本章概要。

表15-1. 本章概要
问题解决方案清单号
存储用户的附加信息定义自定义用户属性1–3, 8–11
更新数据库架构而不删除用户数据执行数据库迁移4–7
执行细粒度授权使用声明(Claims)12–14
添加用户的声明(Claims)使用ClaimsIdentity.AddClaims方法15–19
基于声明(Claims)值授权访问创建一个自定义的授权过滤器注解属性20–21
通过第三方认证安装认证提供器的NuGet包,将请求重定向到该提供器,并指定一个创建用户账号的回调URL。22–25

15.1 准备示例项目

本章打算继续使用第13章创建并在第14章增强的Users项目。对应用程序无需做什么改变,但需要启动应用程序,并确保数据库中有一些用户。图15-1显示了数据库的状态,它含有上一章的用户AdminAliceBob以及Joe。为了检查用户,请启动应用程序,请求/Admin/Index URL,并以Admin用户进行认证。

图15-1

图15-1. Identity数据库中的最初用户

本章还需要一些角色。我用RoleAdmin控制器创建了角色UsersEmployees,并为这些角色指定了一些用户,如表15-2所示。

表15-2. 角色及成员(作者将此表的标题写错了——译者注)
角色成员
UsersAlice, Joe
EmployeesAliceBob

图15-2显示了由RoleAdmin控制器所显示出来的必要的角色配置。

图15-2

图15-2. 配置本章所需的角色

15.2 添加自定义用户属性

我在第13章创建AppUser类来表示用户时曾做过说明,基类定义了一组描述用户的基本属性,如E-mail地址、电话号码等。大多数应用程序还需要存储用户的更多信息,包括持久化应用程序爱好以及地址等细节——简言之,需要存储对运行应用程序有用并且在各次会话之间应当保持的任何数据。在ASP.NET Membership中,这是通过用户资料(User Profile)系统来处理的,但ASP.NET Identity采取了一种不同的办法。

因为ASP.NET Identity默认是使用Entity Framework来存储其数据的,定义附加的用户信息只不过是给用户类添加属性的事情,然后让Code First特性去创建需要存储它们的数据库架构即可。表15-3描述了自定义用户属性的情形。

表15-3. 自定义用户属性的情形
问题回答
什么是自定义用户属性?自定义用户属性让你能够存储附加的用户信息,包括他们的爱好和设置。
为何要关心它?设置的持久化存储意味着,用户不必每次登录到应用程序时都提供同样的信息。
在MVC框架中如何使用它?此特性不是由MVC框架直接使用的,但它在动作方法中使用是有效的。

15.2.1 定义自定义属性

清单15-1演示了如何给AppUser类添加一个简单的属性,用以表示用户生活的城市。

清单15-1. 在AppUser.cs文件中添加属性

using System;
using Microsoft.AspNet.Identity.EntityFramework;
namespace Users.Models {}

这里定义了一个枚举,名称为Cities,它定义了一些大城市的值,另外给AppUser类添加了一个名称为City的属性。为了让用户能够查看和编辑City属性,给Home控制器添加了几个动作方法,如清单15-2所示。

清单15-2. 在HomeController.cs文件中添加对自定义属性的支持

using System.Web.Mvc;
using System.Collections.Generic;
using System.Web;
using System.Security.Principal;
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;
        }

    }
}

我添加了一个CurrentUser属性,它使用AppUserManager类接收了表示当前用户的AppUser实例。在GET版本的UserProps动作方法中,传递了这个AppUser对象作为视图模型。而在POST版的方法中用它更新了City属性的值。清单15-3显示了UserProps.cshtml视图,它显示了City属性的值,并包含一个修改它的表单。

清单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>
}

警告:创建了视图之后不要启动应用程序。在以下小节中,将演示如何保留数据库的内容,如果现在启动应用程序,将会删除ASP.NET Identity的用户。

15.2.2 准备数据库迁移

Entity Framework Code First特性的默认行为是,一旦修改了派生数据库架构的类,便会删除数据库中的数据表,并重新创建它们。在第14章可以看到这种情况,在我添加角色支持时:当重启应用程序后,数据库被重置,用户账号也丢失。

不要启动应用程序,但如果你这么做了,会看到类似的效果。在开发期间删除数据没什么问题,但如果在产品设置中这么做了,通常是灾难性的,因为它会删除所有真实的用户账号,而备份恢复是很痛苦的事。在本小节中,我打算演示如何使用数据库迁移特性,它能以比较温和的方式更新Code First的架构,并保留架构中的已有数据。

第一个步骤是在Visual Studio的“Package Manager Console(包管理器控制台)”中发布以下命令:

Enable-Migrations –EnableAutomaticMigrations

它启用了数据库的迁移支持,并在“Solution Explorer(解决方案资源管理器)”创建一个Migrations文件夹,其中含有一个Configuration.cs类文件,内容如清单15-4所示。

清单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" }
            //    );
            //
        }
    }
}

提示:你可能会觉得奇怪,为什么要在管理NuGet包的控制台中输入数据库迁移的命令?答案是“Package Manager Console(包管理控制台)”是真正的PowerShell,这是Visual studio冒用的一个通用工具。你可以使用此控制台发送大量的有用命令,详见http://go.microsoft.com/fwlink/?LinkID=108518。

这个类将用于把数据库中的现有内容迁移到新的数据库架构,Seed方法的调用为更新现有数据库记录提供了机会。在清单15-5中可以看到,我如何用Seed方法为新的City属性设置默认值,City是添加到AppUser类中自定义属性。(为了体现我一贯的编码风格,我对这个类文件也进行了更新。)

清单15-5. 在Configuration.cs文件中管理已有内容

namespace Users.Migrations {

    internal sealed class Configuration
            : DbMigrationsConfiguration<AppIdentityDbContext> {

        public Configuration() {
            AutomaticMigrationsEnabled = true;
            ContextKey = "Users.Infrastructure.AppIdentityDbContext";
        }

        protected override void Seed(AppIdentityDbContext context) {

        }
    }
}

你可能会注意到,添加到Seed方法中的许多代码取自于IdentityDbInit类,在第14章中我用这个类将管理用户植入了数据库。这是因为这个新添加的、用以支持数据库迁移的Configuration类,将代替IdentityDbInit类的种植功能,我很快便会更新这个类。除了要确保有admin用户之外,在Seed方法中的重要语句是那些为AppUser类的City属性设置初值的语句,如下所示:

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

你不一定要为新属性设置默认值——这里只是想演示Configuration类中的Seed方法,可以用它更新数据库中的已有用户记录。

警告:在用于真实项目的Seed方法中为属性设置值时要小心,因为你每一次修改架构时,都会运用这些值,这会将自执行上一次架构更新之后,用户设置的任何数据覆盖掉。这里设置City属性的值只是为了演示它能够这么做。

修改数据库上下文类

Configuration类中添加种植代码的原因是我需要修改IdentityDbInit类。此时,IdentityDbInit类派生于描述性命名的DropCreateDatabaseIfModelChanges<AppIdentityDbContext> 类,和你相像的一样,它会在Code First类改变时删除整个数据库。清单15-6显示了我对IdentityDbInit类所做的修改,以防止它影响数据库。

清单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();
        }
    }

    }
}

我删除了这个类中所定义的方法,并将它的基类改为NullDatabaseInitializer<AppIdentityDbContext> ,它可以防止架构修改。

15.2.3 执行迁移

剩下的事情只是生成并运用迁移了。首先,在“Package Manager Console(包管理器控制台)”中执行以下命令:

Add-Migration CityProperty

这创建了一个名称为CityProperty的新迁移(我比较喜欢让迁移的名称反映出我所做的修改)。这会在文件夹中添加一个新的类文件,而且其命名会反映出该命令执行的时间以及迁移名称,例如,我的这个文件名称为201402262244036_CityProperty.cs。该文件的内容含有迁移期间Entity Framework修改数据库的细节,如清单15-7所示。

清单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");
        }
    }
}

Up方法描述了在数据库升级时,需要对架构所做的修改,在这个例子中,意味着要在AspNetUsers数据表中添加City数据列,该数据表是ASP.NET Identity数据库用来存储用户记录的。

最后一步是执行迁移。无需启动应用程序,只需在“Package Manager Console(包管理器控制台)”中运行以下命令即可:

Update-Database –TargetMigration CityProperty

这会修改数据库架构,并执行Configuration.Seed方法中的代码。已有用户账号会被保留,且增强了City属性(我在Seed方法中已将其设置为“Paris”)。

15.2.4 测试迁移

为了测试迁移的效果,启动应用程序,导航到/Home/UserProps URL,并以Identity中的用户(例如Alice,口令MySecret)进行认证。一旦已被认证,便会看到该用户City属性的当前值,并可以对其进行修改,如图15-3所示。

图15-3

图15-3. 显示和个性自定义用户属性

15.2.5 定义附加属性

现在,已经建立了数据库迁移,我打算再定义一个属性,这恰恰演示了如何处理持续不断的修改,也为了演示Configuration.Seed方法更有用(至少无害)的示例。清单15-8显示了我在AppUser类上添加了一个Country属性。

清单15-8. 在AppUserModels.cs文件中添加另一个属性

using System;
using Microsoft.AspNet.Identity.EntityFramework; 
namespace Users.Models {

    public enum Cities {
        LONDON, PARIS, CHICAGO
    }

        }
    }
}

我已经添加了一个枚举,它定义了国家名称。还添加了一个辅助器方法,它可以根据City属性选择一个国家。清单15-9显示了对Configuration类所做的修改,以使Seed方法根据City设置Country属性,但只当CountryNONE时才进行设置(在迁移数据库时,所有用户都是NONE,因为Entity Framework会将枚举列设置为枚举的第一个值)。

清单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