定制Asp.NET 5 MVC内建身份验证机制 - 基于自建SQL Server用户/角色数据表的表单身份验证

背景

在需要进行表单认证的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
)
)
GO
ALTER TABLE [dbo].[User] ADD CONSTRAINT [DF_User_IsLocked] DEFAULT ((0)) FOR [IsLocked]
GO
ALTER TABLE [dbo].[User] ADD CONSTRAINT [DF_User_EmailConfirmed] DEFAULT ((0)) FOR [EmailConfirmed]
GO
ALTER TABLE [dbo].[User] ADD CONSTRAINT [DF_User_PhoneNumberConfirmed] DEFAULT ((0)) FOR [PhoneNumberConfirmed]
GO
ALTER TABLE [dbo].[User] ADD CONSTRAINT [DF_User_TwoFactorEnabled] DEFAULT ((0)) FOR [TwoFactorEnabled]
GO
ALTER TABLE [dbo].[User] ADD CONSTRAINT [DF_User_LockoutEnabled] DEFAULT ((0)) FOR [LockoutEnabled]
GO
ALTER TABLE [dbo].[User] ADD CONSTRAINT [DF_User_AccessFailCount] DEFAULT ((0)) FOR [AccessFailedCount]
GO CREATE 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
)
)
GO CREATE TABLE [dbo].[Role] (
[Id] BIGINT IDENTITY (1, 1) NOT NULL,
[Name] NVARCHAR (MAX) NOT NULL,
CONSTRAINT [PK_Role] PRIMARY KEY CLUSTERED ([Id] ASC)
)
GO CREATE 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
)
GO CREATE NONCLUSTERED INDEX [IX_RoleId]
ON [dbo].[UserRole]([RoleId] ASC);
GO CREATE NONCLUSTERED INDEX [IX_UserId]
ON [dbo].[UserRole]([UserId] ASC);
GO CREATE 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
)
GO CREATE NONCLUSTERED INDEX [IX_UserId]
ON [dbo].[UserLogin]([UserId] ASC);
GO CREATE 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
)
GO CREATE NONCLUSTERED INDEX [IX_User_Id]
ON [dbo].[UserClaim]([UserId] ASC); GO

Step 2. 创建MVC示例项目

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

定制Asp.NET 5 MVC内建身份验证机制 - 基于自建SQL Server用户/角色数据表的表单身份验证

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

定制Asp.NET 5 MVC内建身份验证机制 - 基于自建SQL Server用户/角色数据表的表单身份验证

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

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

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

  定制Asp.NET 5 MVC内建身份验证机制 - 基于自建SQL Server用户/角色数据表的表单身份验证

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

  定制Asp.NET 5 MVC内建身份验证机制 - 基于自建SQL Server用户/角色数据表的表单身份验证

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

  定制Asp.NET 5 MVC内建身份验证机制 - 基于自建SQL Server用户/角色数据表的表单身份验证
  
  定制Asp.NET 5 MVC内建身份验证机制 - 基于自建SQL Server用户/角色数据表的表单身份验证

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>
{
}
}
  6.3 创建MyUserRole类
namespace Core.Models
{
public class MyUserRole : IdentityUserRole<long>
{
}
}
  6.4 创建MyClaim类
namespace Core.Models
{
public class MyClaim : IdentityUserClaim<long>
{
}
}
  6.5 创建MyRole类
namespace Core.Models
{
public class MyRole : IdentityRole<long, MyUserRole>
{
}
}
  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 }
}

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 = ,
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 }
}

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();
} #endregion
}
}
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();
}
}
}

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

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; }
} }

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

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>
}
  _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>
}
}
}
  _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>
}
   _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>
}
  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>
  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")
}
  ExternalLoginFailure.cshtml
@{
ViewBag.Title = "Login Failure";
} <h2>@ViewBag.Title.</h2>
<h3 class="text-error">Unsuccessful login with service.</h3>
  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")
}
  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>
  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")
}
  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")
}
  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")
}
  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")
}
  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>

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项目,网站正常运行后,先注册用户:

  定制Asp.NET 5 MVC内建身份验证机制 - 基于自建SQL Server用户/角色数据表的表单身份验证
  注册成功
  定制Asp.NET 5 MVC内建身份验证机制 - 基于自建SQL Server用户/角色数据表的表单身份验证

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

  定制Asp.NET 5 MVC内建身份验证机制 - 基于自建SQL Server用户/角色数据表的表单身份验证

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

 

 
以上项目与数据库脚本可以从我的Github下载:https://github.com/heuyang/IdentityCustomizationExample/archive/master.zip
 
我的QQ: 384059,欢迎和我一样在做企业信息系统的同道中人加我的QQ互相交流,一起成长
 
上一篇:Quartz2D 画大白


下一篇:net core mvc剖析:启动流程