定制与扩展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项目的名称,确定
在接下来的项目设置界面中,选择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文件夹
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 = 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 } }
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 } }
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); } } }
Microsoft.AspNet.Identity提供了IIdentityMessageService接口,MyIdentityEmailService和MyIdentitySmsService都继承了IIdentityMessageService接口,用于向用户发送Email和短信通知
Step 9. 创建ApplicationDbContext.cs
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包
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 { @: } </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);
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 ...... } } }