你的位置:首页 > ASP.net教程

[ASP.net教程]定制Asp.NET 5 MVC内建身份验证机制


背景

在需要进行表单认证的Asp.NET 5 MVC项目被创建后,往往需要根据项目的实际需求做一系列的工作对MVC 5内建的身份验证机制(Asp.NET Identity)进行扩展和定制:

  • Asp.NET内建的身份验证机制会使用Local DB(本地数据库)读写用户相关的信息,而在数据库驱动的项目中,管理业务信息的数据库通常是特定的数据库环境,比如远程SQL Server数据库实例或Access数据库等等,业务数据库中保存着一系列针对业务需求的数据表,因此需要定制MVC 5内建身份验证,使其操作的的用户表们与业务数据库的表们共处在同一数据库中
  • Asp.NET身份验证默认创建的用户表名为:AspNetRoles, AspNetUserClaims, AspNetUserLogins, AspNetUserRoles, AspNetUsers等,与实际业务数据库中自成体系的数据表命名习惯(如tblProduct, PRODUCT, Products...)不一致,因此需要定制MVC 5内建身份验证,使其使用我们指定的表名称保存用户信息,以便与实际业务数据库中的表名称处于相同的命名规范体系
  • 实际业务中用户信息往往多于Asp.NET默认提供的,如根据实际情况会需要以用户email登录,或在Users表中保存用户的guid,性别,地址,是否激活等等,因此需要对Asp.net创建的表,以及相应操作的代码进行扩展

总之,一切都是为了减轻管理的负担,提升工作效率,使项目整体变得更加优雅。

要点

本文仅聚焦在表单身份认证(Forms Authentication)的个性化定制

 

步骤

Step 1. 创建SQL Server数据库,并运行以下SQL,创建示例用户数据表

CREATE TABLE [dbo].[User](  [Id] [bigint] IDENTITY(1,1) NOT NULL,  [Login] [nvarchar](50) NOT NULL,  [EMail] [nvarchar](255) NOT NULL,  [Password] [nvarchar](500) NULL,  [CreationDate] [datetime] NULL,  [ApprovalDate] [datetime] NULL,  [LastLoginDate] [datetime] NULL,  [IsLocked] [bit] NOT NULL,  [PasswordQuestion] [nvarchar](max) NULL,  [PasswordAnswer] [nvarchar](max) NULL,  [ActivationToken] [nvarchar](200) NULL,  [EmailConfirmed] [bit] NOT NULL,  [SecurityStamp] [nvarchar](max) NULL,  [PhoneNumber] [nvarchar](50) NULL,  [PhoneNumberConfirmed] [bit] NOT NULL,  [TwoFactorEnabled] [bit] NOT NULL,  [LockoutEndDateUtc] [datetime2](7) NULL,  [LockoutEnabled] [bit] NOT NULL,  [AccessFailedCount] [int] NOT NULL,  CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED   (    [Id] ASC  ),   CONSTRAINT [UX_User_EMail] UNIQUE NONCLUSTERED   (    [EMail] ASC  ),   CONSTRAINT [UX_User_Login] UNIQUE NONCLUSTERED   (    [Login] ASC  ))GOALTER TABLE [dbo].[User] ADD CONSTRAINT [DF_User_IsLocked] DEFAULT ((0)) FOR [IsLocked]GOALTER TABLE [dbo].[User] ADD CONSTRAINT [DF_User_EmailConfirmed] DEFAULT ((0)) FOR [EmailConfirmed]GOALTER TABLE [dbo].[User] ADD CONSTRAINT [DF_User_PhoneNumberConfirmed] DEFAULT ((0)) FOR [PhoneNumberConfirmed]GOALTER TABLE [dbo].[User] ADD CONSTRAINT [DF_User_TwoFactorEnabled] DEFAULT ((0)) FOR [TwoFactorEnabled]GOALTER TABLE [dbo].[User] ADD CONSTRAINT [DF_User_LockoutEnabled] DEFAULT ((0)) FOR [LockoutEnabled]GOALTER TABLE [dbo].[User] ADD CONSTRAINT [DF_User_AccessFailCount] DEFAULT ((0)) FOR [AccessFailedCount]GOCREATE TABLE [UserRegistrationToken](  [Id] [bigint] IDENTITY(1,1) NOT NULL,  [UserId] [bigint] NULL,  [Token] [nchar](10) NOT NULL,  CONSTRAINT [PK_SecurityToken] PRIMARY KEY CLUSTERED   (    [Id] ASC  ),  CONSTRAINT [UX_UserRegistrationToken_Token] UNIQUE NONCLUSTERED   (    [Token] ASC  ))GOCREATE TABLE [dbo].[Role] (  [Id]  BIGINT IDENTITY (1, 1) NOT NULL,  [Name] NVARCHAR (MAX) NOT NULL,  CONSTRAINT [PK_Role] PRIMARY KEY CLUSTERED ([Id] ASC))GOCREATE TABLE [dbo].[UserRole] (  [UserId] BIGINT NOT NULL,  [RoleId] BIGINT NOT NULL,  CONSTRAINT [PK_UserRole] PRIMARY KEY CLUSTERED ([UserId] ASC, [RoleId] ASC),  CONSTRAINT [FK_UserRole_Role] FOREIGN KEY ([RoleId]) REFERENCES [dbo].[Role] ([Id]) ON DELETE CASCADE,  CONSTRAINT [FK_UserRole_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE)GOCREATE NONCLUSTERED INDEX [IX_RoleId]  ON [dbo].[UserRole]([RoleId] ASC);GOCREATE NONCLUSTERED INDEX [IX_UserId]  ON [dbo].[UserRole]([UserId] ASC);GOCREATE TABLE [dbo].[UserLogin] (  [UserId]    BIGINT NOT NULL,  [LoginProvider] NVARCHAR (128) NOT NULL,  [ProviderKey]  NVARCHAR (128) NOT NULL,  CONSTRAINT [PK_UserLogin] PRIMARY KEY CLUSTERED ([UserId] ASC, [LoginProvider] ASC, [ProviderKey] ASC),  CONSTRAINT [FK_UserLogin_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE)GOCREATE NONCLUSTERED INDEX [IX_UserId]  ON [dbo].[UserLogin]([UserId] ASC);GOCREATE TABLE [dbo].[UserClaim] (  [Id]     BIGINT IDENTITY (1, 1) NOT NULL,  [UserId]   BIGINT NOT NULL,  [ClaimType] NVARCHAR (MAX) NULL,  [ClaimValue] NVARCHAR (MAX) NULL,  CONSTRAINT [PK_UserClaim] PRIMARY KEY CLUSTERED ([Id] ASC),  CONSTRAINT [FK_UserClaim_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE)GOCREATE NONCLUSTERED INDEX [IX_User_Id]  ON [dbo].[UserClaim]([UserId] ASC);GO

View Code

Step 2. 创建MVC示例项目

运行Visual Studio 2013 -> 新建项目 -> Visual C# -> Web -> ASP.NET Web Application,输入MVC项目的名称,确定

在接下来的项目设置界面中,选择MVC项目,认证方式选择"个别用户帐户"

Step 3. 创建单独的类库,用于保存业务模型,数据库关系映射,业务逻辑等

  实际项目中,我个人很喜欢把业务模型,数据库关系映射,业务逻辑等根据实际情况放到独立的类库项目中。即使很小型的简单项目,也会至少把与前端表示层不相关的代码归拢到一个类库里面,便于管理

  解决方案浏览器中右击解决方案节点 -> "添加..." -> 新项目

  

  新建项目窗口中,选择Visual C# -> Windows -> 类库 -> 输入项目名称 (本例中用Core命名) -> 确定 -> 删除自动创建的Class1.cs

  

Step 4. 更新MVC项目中的数据库连接字符串

  因为我们的目标是使用自己的数据库而非Asp.NET默认的,因此需要首先修改MVC项目中的连接字符串

  打开Web.config,找到<connectionStrings>节点,对名为DefaultConnection的connectionString进行修改:

<add name="DefaultConnection" connectionString="Server=myserver;Database=mydatabase;User Id=myuserid;Password=mypassword;" providerName="System.Data.SqlClient" />

Step 5. 在类库项目中引用所需的Nuget包

  Microsoft ASP.NET Identity Owin和Microsoft ASP.NET Identity Framework,本项目中引用的这两个包的版本为2.2.1


  
  
  

Step 6. 在类库项目中创建Models

  6.1 创建Models文件夹
    该文件夹用于保存用户验证相关的模型类,这些类都继承自Microsoft.AspNet.Identity.EntityFramework命名空间下相应的类,并显示指定了关键字的类型为long(Asp.NET默认使用string类型)
  6.2 创建MyLogin类
namespace Core.Models{  public class MyLogin : IdentityUserLogin<long>  {  }}

View Code
  6.3 创建MyUserRole类
namespace Core.Models{  public class MyUserRole : IdentityUserRole<long>  {  }}

View Code
  6.4 创建MyClaim类
namespace Core.Models{  public class MyClaim : IdentityUserClaim<long>  {  }}

View Code
  6.5 创建MyRole类
namespace Core.Models{  public class MyRole : IdentityRole<long, MyUserRole>  {  }}

View Code
  6.6 创建MyUser类
namespace Core.Models{  public class MyUser : IdentityUser<long, MyLogin, MyUserRole, MyClaim>  {    #region properties    public string ActivationToken { get; set; }    public string PasswordAnswer { get; set; }    public string PasswordQuestion { get; set; }    #endregion    #region methods    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(MyUserManager userManager)    {      var userIdentity = await userManager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);      // Add custom user claims here      return userIdentity;    }    #endregion  }}

View Code

Step 7. 创建MyUserManager类


namespace Core.Models{  public class MyUserManager : UserManager<MyUser, long>  {    #region constructors and destructors    public MyUserManager(IUserStore<MyUser, long> store)      : base(store)    {    }    #endregion    #region methods    public static MyUserManager Create(IdentityFactoryOptions<MyUserManager> options, IOwinContext context)    {      var manager = new MyUserManager(new UserStore<MyUser, MyRole, long, MyLogin, MyUserRole, MyClaim>(context.Get<ApplicationDbContext>()));      // Configure validation logic for usernames      manager.UserValidator = new UserValidator<MyUser, long>(manager)      {        AllowOnlyAlphanumericUserNames = false,        RequireUniqueEmail = true      };      // Configure validation logic for passwords      manager.PasswordValidator = new PasswordValidator      {        RequiredLength = 6,        RequireNonLetterOrDigit = true,        RequireDigit = true,        RequireLowercase = true,        RequireUppercase = true,      };      // Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user      // You can write your own provider and plug in here.      manager.RegisterTwoFactorProvider(        "PhoneCode",        new PhoneNumberTokenProvider<MyUser, long>        {          MessageFormat = "Your security code is: {0}"        });      manager.RegisterTwoFactorProvider(        "EmailCode",        new EmailTokenProvider<MyUser, long>        {          Subject = "Security Code",          BodyFormat = "Your security code is: {0}"        });      manager.EmailService = new MyIdentityEmailService();      manager.SmsService = new MyIdentitySmsService(); ;      var dataProtectionProvider = options.DataProtectionProvider;      if (dataProtectionProvider != null)      {        manager.UserTokenProvider = new DataProtectorTokenProvider<MyUser, long>(dataProtectionProvider.Create("ASP.NET Identity"));      }      return manager;    }    #endregion  }}

View Code

Step 8. 创建MyIdentityEmailService.cs和MyIdentitySmsService.cs

namespace Core{  public class MyIdentityEmailService : IIdentityMessageService  {    #region methods    public Task SendAsync(IdentityMessage message)    {      // Plug in your email service here to send an email.      return Task.FromResult(0);    }    #endregion  }}

View Code
namespace Core.Models{  public class MyIdentitySmsService : IIdentityMessageService  {    public Task SendAsync(IdentityMessage message)    {      // Plug in your sms service here to send a text message.      return Task.FromResult(0);    }  }}

View Code

  Microsoft.AspNet.Identity提供了IIdentityMessageService接口,MyIdentityEmailService和MyIdentitySmsService都继承了IIdentityMessageService接口,用于向用户发送Email和短信通知


Step 9. 创建ApplicationDbContext.cs

  Asp.NET Identity使用Entityframework作为用户数据库的ORM,ApplicationDbContext继承了Microsoft.AspNet.Identity.EntityFramework.IdentityDbContext,并将我们刚刚创建的那些类指定为DbContext的操作对象
namespace Core{  public class ApplicationDbContext : IdentityDbContext<MyUser, MyRole, long, MyLogin, MyUserRole, MyClaim>  {    #region constructors and destructors    public ApplicationDbContext()      : base("DefaultConnection")    {    }    #endregion    #region methods    public static ApplicationDbContext Create()    {      return new ApplicationDbContext();    }    protected override void OnModelCreating(DbModelBuilder modelBuilder)    {      base.OnModelCreating(modelBuilder);      // Map Entities to their tables.      modelBuilder.Entity<MyUser>().ToTable("User");      modelBuilder.Entity<MyRole>().ToTable("Role");      modelBuilder.Entity<MyClaim>().ToTable("UserClaim");      modelBuilder.Entity<MyLogin>().ToTable("UserLogin");      modelBuilder.Entity<MyUserRole>().ToTable("UserRole");      // Set AutoIncrement-Properties      modelBuilder.Entity<MyUser>().Property(r => r.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);      modelBuilder.Entity<MyClaim>().Property(r => r.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);      modelBuilder.Entity<MyRole>().Property(r => r.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);      // Override some column mappings that do not match our default      modelBuilder.Entity<MyUser>().Property(r => r.UserName).HasColumnName("Login");      modelBuilder.Entity<MyUser>().Property(r => r.PasswordHash).HasColumnName("Password");    }    #endregion  }}

View Code

Step 9. 在MVC项目中添加对Core项目的引用


Step 10. 通过Buget移除并重新添加Microsoft ASP.NET Identity Owin和Microsoft ASP.NET Identity Framework包

  因为在Core项目中引用到的这两个包的版本高于Asp.NET MVC默认提供的版本,因此需要重新添加对它们的引用,保持版本一致性
 

Step 11. 修改默认Asp.net MVC项目中与用户验证相关的ViewModel,View和Controller,使其使用我们自建的模型、UserNamager与DbContext。首先从ViewModel开始,打开MVC项目下Models文件夹中的AccountViewModels.cs,修改后的文件如下所示

using System.ComponentModel.DataAnnotations;namespace MyMvcProject.Models{  public class ExternalLoginConfirmationViewModel  {    [Required]    [EmailAddress]    [Display(Name = "Email")]    public string Email { get; set; }  }  public class ExternalLoginListViewModel  {    public string Action { get; set; }    public string ReturnUrl { get; set; }  }  public class ManageUserViewModel  {    [Required]    [DataType(DataType.Password)]    [Display(Name = "Current password")]    public string OldPassword { get; set; }    [Required]    [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]    [DataType(DataType.Password)]    [Display(Name = "New password")]    public string NewPassword { get; set; }    [DataType(DataType.Password)]    [Display(Name = "Confirm new password")]    [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]    public string ConfirmPassword { get; set; }  }  public class LoginViewModel  {    [Required]    [EmailAddress]    [Display(Name = "Email")]    public string Email { get; set; }    [Required]    [DataType(DataType.Password)]    [Display(Name = "Password")]    public string Password { get; set; }    [Display(Name = "Remember me?")]    public bool RememberMe { get; set; }  }  public class RegisterViewModel  {    [Required]    [EmailAddress]    [Display(Name = "Email")]    public string Email { get; set; }    [Required]    [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]    [DataType(DataType.Password)]    [Display(Name = "Password")]    public string Password { get; set; }    [DataType(DataType.Password)]    [Display(Name = "Confirm password")]    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]    public string ConfirmPassword { get; set; }  }  public class ForgotPasswordViewModel  {    [Required]    [EmailAddress]    [Display(Name = "Email")]    public string Email { get; set; }  }  public class ResetPasswordViewModel  {    [Required]    [EmailAddress]    [Display(Name = "Email")]    public string Email { get; set; }    [Required]    [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]    [DataType(DataType.Password)]    [Display(Name = "Password")]    public string Password { get; set; }    [DataType(DataType.Password)]    [Display(Name = "Confirm password")]    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]    public string ConfirmPassword { get; set; }    public string Code { get; set; }  }}

View Code

Step 12. 接下来是Controller, 打开MVC项目下Controllers文件夹中的AccountController.cs,修改后的文件如下所示

using System;using System.Threading.Tasks;using System.Web;using System.Web.Mvc;using Core.Models;using Microsoft.AspNet.Identity;using Microsoft.AspNet.Identity.Owin;using Microsoft.Owin.Security;using MyMvcProject.Models;namespace MyMvcProject.Controllers{  [Authorize]  public class AccountController : Controller  {    #region constants    private const string XsrfKey = "XsrfId";    #endregion    #region member vars    private MyUserManager _userManager;    #endregion    #region enums    public enum ManageMessageId    {      ChangePasswordSuccess,      SetPasswordSuccess,      RemoveLoginSuccess,      Error    }    #endregion    #region properties    public MyUserManager UserManager    {      get      {        return _userManager ?? HttpContext.GetOwinContext().GetUserManager<MyUserManager>();      }      private set      {        _userManager = value;      }    }    private IAuthenticationManager AuthenticationManager    {      get      {        return HttpContext.GetOwinContext().Authentication;      }    }    #endregion    #region constructors and destructors    public AccountController()    {    }    public AccountController(MyUserManager userManager)    {      UserManager = userManager;    }    protected override void Dispose(bool disposing)    {      if (disposing && UserManager != null)      {        UserManager.Dispose();        UserManager = null;      }      base.Dispose(disposing);    }    #endregion    #region methods    [AllowAnonymous]    public async Task<ActionResult> ConfirmEmail(long userId, string code)    {      if (userId == null || code == null)      {        return View("Error");      }      var result = await UserManager.ConfirmEmailAsync(userId, code);      if (result.Succeeded)      {        return View("ConfirmEmail");      }      AddErrors(result);      return View();    }    //    // POST: /Account/Disassociate    [HttpPost]    [ValidateAntiForgeryToken]    public async Task<ActionResult> Disassociate(string loginProvider, string providerKey)    {      ManageMessageId? message = null;      var result = await UserManager.RemoveLoginAsync(long.Parse(User.Identity.GetUserId()), new UserLoginInfo(loginProvider, providerKey));      if (result.Succeeded)      {        var user = await UserManager.FindByIdAsync(long.Parse(User.Identity.GetUserId()));        await SignInAsync(user, false);        message = ManageMessageId.RemoveLoginSuccess;      }      else      {        message = ManageMessageId.Error;      }      return RedirectToAction(        "Manage",        new        {          Message = message        });    }    //    // POST: /Account/ExternalLogin    [HttpPost]    [AllowAnonymous]    [ValidateAntiForgeryToken]    public ActionResult ExternalLogin(string provider, string returnUrl)    {      // Request a redirect to the external login provider      return new ChallengeResult(        provider,        Url.Action(          "ExternalLoginCallback",          "Account",          new          {            ReturnUrl = returnUrl          }));    }    //    // GET: /Account/ExternalLoginCallback    [AllowAnonymous]    public async Task<ActionResult> ExternalLoginCallback(string returnUrl)    {      var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();      if (loginInfo == null)      {        return RedirectToAction("Login");      }      // Sign in the user with this external login provider if the user already has a login      var user = await UserManager.FindAsync(loginInfo.Login);      if (user != null)      {        await SignInAsync(user, false);        return RedirectToLocal(returnUrl);      }      // If the user does not have an account, then prompt the user to create an account      ViewBag.ReturnUrl = returnUrl;      ViewBag.LoginProvider = loginInfo.Login.LoginProvider;      return View(        "ExternalLoginConfirmation",        new ExternalLoginConfirmationViewModel        {          Email = loginInfo.Email        });    }    //    // POST: /Account/ExternalLoginConfirmation    [HttpPost]    [AllowAnonymous]    [ValidateAntiForgeryToken]    public async Task<ActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl)    {      if (User.Identity.IsAuthenticated)      {        return RedirectToAction("Manage");      }      if (ModelState.IsValid)      {        // Get the information about the user from the external login provider        var info = await AuthenticationManager.GetExternalLoginInfoAsync();        if (info == null)        {          return View("ExternalLoginFailure");        }        var user = new MyUser        {          UserName = model.Email,          Email = model.Email        };        var result = await UserManager.CreateAsync(user);        if (result.Succeeded)        {          result = await UserManager.AddLoginAsync(user.Id, info.Login);          if (result.Succeeded)          {            await SignInAsync(user, false);            // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771            // Send an email with this link            // string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);            // var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);            // SendEmail(user.Email, callbackUrl, "Confirm your account", "Please confirm your account by clicking this link");            return RedirectToLocal(returnUrl);          }        }        AddErrors(result);      }      ViewBag.ReturnUrl = returnUrl;      return View(model);    }    //    // GET: /Account/ExternalLoginFailure    [AllowAnonymous]    public ActionResult ExternalLoginFailure()    {      return View();    }    [AllowAnonymous]    public ActionResult ForgotPassword()    {      return View();    }    //    // POST: /Account/ForgotPassword    [HttpPost]    [AllowAnonymous]    [ValidateAntiForgeryToken]    public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)    {      if (ModelState.IsValid)      {        var user = await UserManager.FindByNameAsync(model.Email);        if (user == null || !(await UserManager.IsEmailConfirmedAsync(user.Id)))        {          ModelState.AddModelError("", "The user either does not exist or is not confirmed.");          return View();        }        // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771        // Send an email with this link        // string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);        // var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);            // await UserManager.SendEmailAsync(user.Id, "Reset Password", "Please reset your password by clicking <a href=\"" + callbackUrl + "\">here</a>");        // return RedirectToAction("ForgotPasswordConfirmation", "Account");      }      // If we got this far, something failed, redisplay form      return View(model);    }    //    // GET: /Account/ForgotPasswordConfirmation    [AllowAnonymous]    public ActionResult ForgotPasswordConfirmation()    {      return View();    }    //    // POST: /Account/LinkLogin    [HttpPost]    [ValidateAntiForgeryToken]    public ActionResult LinkLogin(string provider)    {      // Request a redirect to the external login provider to link a login for the current user      return new ChallengeResult(provider, Url.Action("LinkLoginCallback", "Account"), User.Identity.GetUserId());    }    //    // GET: /Account/LinkLoginCallback    public async Task<ActionResult> LinkLoginCallback()    {      var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(XsrfKey, User.Identity.GetUserId());      if (loginInfo == null)      {        return RedirectToAction(          "Manage",          new          {            Message = ManageMessageId.Error          });      }      var result = await UserManager.AddLoginAsync(long.Parse(User.Identity.GetUserId()), loginInfo.Login);      if (result.Succeeded)      {        return RedirectToAction("Manage");      }      return RedirectToAction(        "Manage",        new        {          Message = ManageMessageId.Error        });    }    //    // POST: /Account/LogOff    [HttpPost]    [ValidateAntiForgeryToken]    public ActionResult LogOff()    {      AuthenticationManager.SignOut();      return RedirectToAction("Index", "Home");    }    //    // GET: /Account/Login    [AllowAnonymous]    public ActionResult Login(string returnUrl)    {      ViewBag.ReturnUrl = returnUrl;      return View();    }    //    // POST: /Account/Login    [HttpPost]    [AllowAnonymous]    [ValidateAntiForgeryToken]    public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)    {      if (ModelState.IsValid)      {        var user = await UserManager.FindAsync(model.Email, model.Password);        if (user != null)        {          await SignInAsync(user, model.RememberMe);          return RedirectToLocal(returnUrl);        }        ModelState.AddModelError("", "Invalid username or password.");      }      // If we got this far, something failed, redisplay form      return View(model);    }    //    // GET: /Account/Manage    public ActionResult Manage(ManageMessageId? message)    {      ViewBag.StatusMessage =        message == ManageMessageId.ChangePasswordSuccess ? "Your password has been changed."        : message == ManageMessageId.SetPasswordSuccess ? "Your password has been set."        : message == ManageMessageId.RemoveLoginSuccess ? "The external login was removed."        : message == ManageMessageId.Error ? "An error has occurred."        : "";      ViewBag.HasLocalPassword = HasPassword();      ViewBag.ReturnUrl = Url.Action("Manage");      return View();    }    //    // POST: /Account/Manage    [HttpPost]    [ValidateAntiForgeryToken]    public async Task<ActionResult> Manage(ManageUserViewModel model)    {      var hasPassword = HasPassword();      ViewBag.HasLocalPassword = hasPassword;      ViewBag.ReturnUrl = Url.Action("Manage");      if (hasPassword)      {        if (ModelState.IsValid)        {          var result = await UserManager.ChangePasswordAsync(long.Parse(User.Identity.GetUserId()), model.OldPassword, model.NewPassword);          if (result.Succeeded)          {            var user = await UserManager.FindByIdAsync(long.Parse(User.Identity.GetUserId()));            await SignInAsync(user, false);            return RedirectToAction(              "Manage",              new              {                Message = ManageMessageId.ChangePasswordSuccess              });          }          AddErrors(result);        }      }      else      {        // User does not have a password so remove any validation errors caused by a missing OldPassword field        var state = ModelState["OldPassword"];        if (state != null)        {          state.Errors.Clear();        }        if (ModelState.IsValid)        {          var result = await UserManager.AddPasswordAsync(long.Parse(User.Identity.GetUserId()), model.NewPassword);          if (result.Succeeded)          {            return RedirectToAction(              "Manage",              new              {                Message = ManageMessageId.SetPasswordSuccess              });          }          AddErrors(result);        }      }      // If we got this far, something failed, redisplay form      return View(model);    }    //    // GET: /Account/Register    [AllowAnonymous]    public ActionResult Register()    {      return View();    }    //    // POST: /Account/Register    [HttpPost]    [AllowAnonymous]    [ValidateAntiForgeryToken]    public async Task<ActionResult> Register(RegisterViewModel model)    {      if (ModelState.IsValid)      {        var user = new MyUser        {          UserName = model.Email,          Email = model.Email        };        try        {          var result = await UserManager.CreateAsync(user, model.Password);          if (result.Succeeded)          {            await SignInAsync(user, false);            // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771            // Send an email with this link            // string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);            // var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);            // await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>");            return RedirectToAction("Index", "Home");          }          AddErrors(result);        }        catch (Exception ex)        {          throw (ex);        }      }      // If we got this far, something failed, redisplay form      return View(model);    }    [ChildActionOnly]    public ActionResult RemoveAccountList()    {      var linkedAccounts = UserManager.GetLogins(long.Parse(User.Identity.GetUserId()));      ViewBag.ShowRemoveButton = HasPassword() || linkedAccounts.Count > 1;      return PartialView("_RemoveAccountPartial", linkedAccounts);    }    [AllowAnonymous]    public ActionResult ResetPassword(string code)    {      if (code == null)      {        return View("Error");      }      return View();    }    //    // POST: /Account/ResetPassword    [HttpPost]    [AllowAnonymous]    [ValidateAntiForgeryToken]    public async Task<ActionResult> ResetPassword(ResetPasswordViewModel model)    {      if (ModelState.IsValid)      {        var user = await UserManager.FindByNameAsync(model.Email);        if (user == null)        {          ModelState.AddModelError("", "No user found.");          return View();        }        var result = await UserManager.ResetPasswordAsync(user.Id, model.Code, model.Password);        if (result.Succeeded)        {          return RedirectToAction("ResetPasswordConfirmation", "Account");        }        AddErrors(result);        return View();      }      // If we got this far, something failed, redisplay form      return View(model);    }    //    // GET: /Account/ResetPasswordConfirmation    [AllowAnonymous]    public ActionResult ResetPasswordConfirmation()    {      return View();    }    #endregion    #region Helpers    private async Task SignInAsync(MyUser user, bool isPersistent)    {      AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);      var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);      AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);    }    private void AddErrors(IdentityResult result)    {      foreach (var error in result.Errors)      {        ModelState.AddModelError("", error);      }    }    private bool HasPassword()    {      var user = UserManager.FindById(long.Parse(User.Identity.GetUserId()));      if (user != null)      {        return user.PasswordHash != null;      }      return false;    }    private ActionResult RedirectToLocal(string returnUrl)    {      if (Url.IsLocalUrl(returnUrl))      {        return Redirect(returnUrl);      }      else      {        return RedirectToAction("Index", "Home");      }    }    #endregion    private class ChallengeResult : HttpUnauthorizedResult    {      #region constructors and destructors      public ChallengeResult(string provider, string redirectUri)        : this(provider, redirectUri, null)      {      }      public ChallengeResult(string provider, string redirectUri, string userId)      {        LoginProvider = provider;        RedirectUri = redirectUri;        UserId = userId;      }      #endregion      #region properties      public string LoginProvider { get; set; }      public string RedirectUri { get; set; }      public string UserId { get; set; }      #endregion      #region methods      public override void ExecuteResult(ControllerContext context)      {        var properties = new AuthenticationProperties        {          RedirectUri = RedirectUri        };        if (UserId != null)        {          properties.Dictionary[XsrfKey] = UserId;        }        context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider);      }      #endregion    }  }}

View Code

Step 13. 最后是Views,涉及的文件稍多,但都位于\Views\Account目录下


   _ChangePasswordPartial.cshtml
@using Microsoft.AspNet.Identity@model MyMvcProject.Models.ManageUserViewModel<p>You're logged in as <strong>@User.Identity.GetUserName()</strong>.</p>@using (Html.BeginForm("Manage", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" })){  @Html.AntiForgeryToken()  <h4>Change Password Form</h4>  <hr />  @Html.ValidationSummary()  <div class="form-group">    @Html.LabelFor(m => m.OldPassword, new { @class = "col-md-2 control-label" })    <div class="col-md-10">      @Html.PasswordFor(m => m.OldPassword, new { @class = "form-control" })    </div>  </div>  <div class="form-group">    @Html.LabelFor(m => m.NewPassword, new { @class = "col-md-2 control-label" })    <div class="col-md-10">      @Html.PasswordFor(m => m.NewPassword, new { @class = "form-control" })    </div>  </div>  <div class="form-group">    @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" })    <div class="col-md-10">      @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })    </div>  </div>  <div class="form-group">    <div class="col-md-offset-2 col-md-10">      <input type="submit" value="Change password" class="btn btn-default" />    </div>  </div>}

View Code
  _ExternalLoginsListPartial.cshtml
@using Microsoft.Owin.Security<h4>Use another service to log in.</h4><hr />@{  var loginProviders = Context.GetOwinContext().Authentication.GetExternalAuthenticationTypes();  if (loginProviders.Count() == 0)  {    <div>      <p>        There are no external authentication services configured. See <a href="http://go.microsoft.com/fwlink/?LinkId=313242">this article</a>        for details on setting up this ASP.NET application to support logging in via external services.      </p>    </div>  }  else  {    string action = Model.Action;    string returnUrl = Model.ReturnUrl;    using (Html.BeginForm(action, "Account", new { ReturnUrl = returnUrl }))    {      @Html.AntiForgeryToken()      <div id="socialLoginList">        <p>          @foreach (AuthenticationDescription p in loginProviders)          {            <button type="submit" class="btn btn-default" id="@p.AuthenticationType" name="provider" value="@p.AuthenticationType" title="Log in using your @p.Caption account">@p.AuthenticationType</button>          }        </p>      </div>    }  }}

View Code
  _RemoveAccountPartial.cshtml
@model ICollection<Microsoft.AspNet.Identity.UserLoginInfo>@if (Model.Count > 0){  <h4>Registered Logins</h4>  <table class="table">    <tbody>      @foreach (var account in Model)      {        <tr>          <td>@account.LoginProvider</td>          <td>            @if (ViewBag.ShowRemoveButton)            {              using (Html.BeginForm("Disassociate", "Account"))              {                @Html.AntiForgeryToken()                <div>                  @Html.Hidden("loginProvider", account.LoginProvider)                  @Html.Hidden("providerKey", account.ProviderKey)                  <input type="submit" class="btn btn-default" value="Remove" title="Remove this @account.LoginProvider login from your account" />                </div>              }            }            else            {              @: &nbsp;            }          </td>        </tr>      }    </tbody>  </table>}

View Code
   _SetPasswordPartial.cshtml
@model MyMvcProject.Models.ManageUserViewModel<p class="text-info">  You do not have a local username/password for this site. Add a local  account so you can log in without an external login.</p>@using (Html.BeginForm("Manage", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" })){  @Html.AntiForgeryToken()  <h4>Create Local Login</h4>  <hr />  @Html.ValidationSummary()  <div class="form-group">    @Html.LabelFor(m => m.NewPassword, new { @class = "col-md-2 control-label" })    <div class="col-md-10">      @Html.PasswordFor(m => m.NewPassword, new { @class = "form-control" })    </div>  </div>  <div class="form-group">    @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" })    <div class="col-md-10">      @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })    </div>  </div>  <div class="form-group">    <div class="col-md-offset-2 col-md-10">      <input type="submit" value="Set password" class="btn btn-default" />    </div>  </div>}

View Code
  ConfirmEmail.cshtml
@{  ViewBag.Title = "ConfirmAccount";}<h2>@ViewBag.Title.</h2><div>  <p>    Thank you for confirming your account. Please @Html.ActionLink("click here to log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })  </p></div>

View Code
  ExternalLoginConfirmation.cshtml
@model MyMvcProject.Models.ExternalLoginConfirmationViewModel@{  ViewBag.Title = "Register";}<h2>@ViewBag.Title.</h2><h3>Associate your @ViewBag.LoginProvider account.</h3>@using (Html.BeginForm("ExternalLoginConfirmation", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })){  @Html.AntiForgeryToken()  <h4>Association Form</h4>  <hr />  @Html.ValidationSummary(true, "", new { @class = "text-danger" })  <p class="text-info">    You've successfully authenticated with <strong>@ViewBag.LoginProvider</strong>.    Please enter a user name for this site below and click the Register button to finish    logging in.  </p>  <div class="form-group">    @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })    <div class="col-md-10">      @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })      @Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" })    </div>  </div>  <div class="form-group">    <div class="col-md-offset-2 col-md-10">      <input type="submit" class="btn btn-default" value="Register" />    </div>  </div>}@section Scripts {  @Scripts.Render("~/bundles/jqueryval")}

View Code
  ExternalLoginFailure.cshtml
@{  ViewBag.Title = "Login Failure";}<h2>@ViewBag.Title.</h2><h3 class="text-error">Unsuccessful login with service.</h3>

View Code
  ForgotPassword.cshtml
@model MyMvcProject.Models.ForgotPasswordViewModel@{  ViewBag.Title = "Forgot your password?";}<h2>@ViewBag.Title.</h2>@using (Html.BeginForm("ForgotPassword", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" })){  @Html.AntiForgeryToken()  <h4>Enter your email.</h4>  <hr />  @Html.ValidationSummary("", new { @class = "text-danger" })  <div class="form-group">    @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })    <div class="col-md-10">      @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })    </div>  </div>  <div class="form-group">    <div class="col-md-offset-2 col-md-10">      <input type="submit" class="btn btn-default" value="Email Link" />    </div>  </div>}@section Scripts {  @Scripts.Render("~/bundles/jqueryval")}

View Code
  ForgotPasswordConfirmation.cshtml
@{  ViewBag.Title = "Forgot Password Confirmation";}<hgroup class="title">  <h1>@ViewBag.Title.</h1></hgroup><div>  <p>    Please check your email to reset your password.  </p></div>

View Code
  Login.cshtml
@using MyMvcProject.Models@model LoginViewModel@{  ViewBag.Title = "Log in";}<h2>@ViewBag.Title.</h2><div class="row">  <div class="col-md-8">    <section id="loginForm">      @using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))      {        @Html.AntiForgeryToken()        <h4>Use a local account to log in.</h4>        <hr />        @Html.ValidationSummary(true, "", new { @class = "text-danger" })        <div class="form-group">          @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })          <div class="col-md-10">            @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })            @Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" })          </div>        </div>        <div class="form-group">          @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })          <div class="col-md-10">            @Html.PasswordFor(m => m.Password, new { @class = "form-control" })            @Html.ValidationMessageFor(m => m.Password, "", new { @class = "text-danger" })          </div>        </div>        <div class="form-group">          <div class="col-md-offset-2 col-md-10">            <div class="checkbox">              @Html.CheckBoxFor(m => m.RememberMe)              @Html.LabelFor(m => m.RememberMe)            </div>          </div>        </div>        <div class="form-group">          <div class="col-md-offset-2 col-md-10">            <input type="submit" value="Log in" class="btn btn-default" />          </div>        </div>        <p>          @Html.ActionLink("Register as a new user", "Register")        </p>        @* Enable this once you have account confirmation enabled for password reset functionality          <p>            @Html.ActionLink("Forgot your password?", "ForgotPassword")          </p>*@      }    </section>  </div>  <div class="col-md-4">    <section id="socialLoginForm">      @Html.Partial("_ExternalLoginsListPartial", new ExternalLoginListViewModel { Action = "ExternalLogin", ReturnUrl = ViewBag.ReturnUrl })    </section>  </div></div>@section Scripts {  @Scripts.Render("~/bundles/jqueryval")}

View Code
  Manage.cshtml
@{  ViewBag.Title = "Manage Account";}<h2>@ViewBag.Title.</h2><p class="text-success">@ViewBag.StatusMessage</p><div class="row">  <div class="col-md-12">    @if (ViewBag.HasLocalPassword)    {      @Html.Partial("_ChangePasswordPartial")    }    else    {      @Html.Partial("_SetPasswordPartial")    }    <section id="externalLogins">      @Html.Action("RemoveAccountList")      @Html.Partial("_ExternalLoginsListPartial", new { Action = "LinkLogin", ReturnUrl = ViewBag.ReturnUrl })    </section>  </div></div>@section Scripts {  @Scripts.Render("~/bundles/jqueryval")}

View Code
  Register.cshtml
@model MyMvcProject.Models.RegisterViewModel@{  ViewBag.Title = "Register";}<h2>@ViewBag.Title.</h2>@using (Html.BeginForm("Register", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" })){  @Html.AntiForgeryToken()  <h4>Create a new account.</h4>  <hr />  @Html.ValidationSummary("", new { @class = "text-danger" })  <div class="form-group">    @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })    <div class="col-md-10">      @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })    </div>  </div>  <div class="form-group">    @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })    <div class="col-md-10">      @Html.PasswordFor(m => m.Password, new { @class = "form-control" })    </div>  </div>  <div class="form-group">    @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" })    <div class="col-md-10">      @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })    </div>  </div>  <div class="form-group">    <div class="col-md-offset-2 col-md-10">      <input type="submit" class="btn btn-default" value="Register" />    </div>  </div>}@section Scripts {  @Scripts.Render("~/bundles/jqueryval")}

View Code
  ResetPassword.cshtml
@model MyMvcProject.Models.ResetPasswordViewModel@{  ViewBag.Title = "Reset password";}<h2>@ViewBag.Title.</h2>@using (Html.BeginForm("ResetPassword", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" })){  @Html.AntiForgeryToken()  <h4>Reset your password.</h4>  <hr />  @Html.ValidationSummary("", new { @class = "text-danger" })  @Html.HiddenFor(model => model.Code)  <div class="form-group">    @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })    <div class="col-md-10">      @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })    </div>  </div>  <div class="form-group">    @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })    <div class="col-md-10">      @Html.PasswordFor(m => m.Password, new { @class = "form-control" })    </div>  </div>  <div class="form-group">    @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" })    <div class="col-md-10">      @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })    </div>  </div>  <div class="form-group">    <div class="col-md-offset-2 col-md-10">      <input type="submit" class="btn btn-default" value="Reset" />    </div>  </div>}@section Scripts {  @Scripts.Render("~/bundles/jqueryval")}

View Code
  ResetPasswordConfirmation.cshtml
@{  ViewBag.Title = "Reset password confirmation";}<hgroup class="title">  <h1>@ViewBag.Title.</h1></hgroup><div>  <p>    Your password has been reset. Please @Html.ActionLink("click here to log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })  </p></div>

View Code

Step 14. 以下两行必须添加到\App_Start\Startup.Auth.cs中,以便在MVC程序初始化时创建ApplicationDbContext和MyUserManager的单例

  app.CreatePerOwinContext(ApplicationDbContext.Create);
  app.CreatePerOwinContext<MyUserManager>(MyUserManager.Create);


  修改后的Startup.Auth.cs文件如下:
namespace MyMvcProject{  public partial class Startup  {    // For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864    public void ConfigureAuth(IAppBuilder app)    {      // Configure the db context and user manager to use a single instance per request      app.CreatePerOwinContext(ApplicationDbContext.Create);      app.CreatePerOwinContext<MyUserManager>(MyUserManager.Create);      //other codes      ......      }  }}


Step 15. 试运行MVC项目,网站正常运行后,先注册用户:

  
  注册成功
  

Step 16. 检查目标数据库,用户数据已保存至Web.config文件所指定的目标数据库User表中

  

Step 17. 至此,针对自备数据库的基于表单认证的Asp.net Identity定制的工作已经完成