ASP.NET Identity教程二:(用户管理3)用户登录和邮箱验证功能
三.用户登录
当用户在你的网站上完成注册之后,下次再光顾你的网站时,只需要登录一下即可,登录时需要提供帐号和密码。还有在用户注册时,如果注册成功,也需要执行一下登录操作,在 7.1 节的注册用户逻辑中,没有实现登录操作,在本节中会完成登录操作。登录成功后,会在顶部导航栏上显示用户的帐号和注销按钮,表示当前用户已经登录该网站.
在 ASP.NET Identity 中,使用登录 API 就可以轻松的完成登录,不需要我们编写任何额外的登录逻辑代码,包括用户在客户端 Cookies 中的存储,都是由 ASP.NETIdentity 来完成的。
3.2. 继承 SignInManager<User, string>
在 ASP.NET Identity 中实现用户登录,需要配置应用程序登录管理器。应用程序登录管理器是继承了 SignInManager<User, string>的类来完成的。
在“App_Start”文件夹中的“IdentityConfig.cs”文件中的“Yidosoft.Identity”名称空间下编写如下代码::
/// <summary> /// 配置应用程序登录管理器 /// </summary> public class SignInManager : SignInManager<User, string> { public SignInManager(UserManager userManager, IAuthenticationManager authenticationManager) : base(userManager, authenticationManager) { } public override Task<ClaimsIdentity> CreateUserIdentityAsync(User user) { return user.GenerateUserIdentityAsync((UserManager)UserManager); } public static SignInManager Create(IdentityFactoryOptions<SignInManager> options, IOwinContext context) { return new SignInManager(context.GetUserManager<UserManager>(), context.Authentication); } }
其实这个内容是一开始就在前面教程中进行了使用。
然后再在“Models”文件夹中打开“User.cs”文件,并在 User 类中添加如下方法,其实上次已加入了。
public class User: IdentityUser { public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<User> manager) { // 请注意,authenticationType 必须与 CookieAuthenticationOptions.AuthenticationType 中定义的相应项匹配 var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie); // 在此处添加自定义用户声明 return userIdentity; } #region 添加字段 [Display(Name = "微信")] public virtual string WX { get; set; }
在此代码可以看出,ASP.NET Identity 是完全支持基于声明的身份验证的
.3. 注册 SignInManager
配置好的应用程序登录管理器,还需要在“Startup”类中注册一下,打开“App_Start”文件夹下的“Startup.Auth.cs”文件,然后添加如下代码:
app.CreatePerOwinContext<SignInManager>(SignInManager.Create);
4..4. 编写 LoginViewModel
在“ViewModels”文件夹中添加一个名称为“LoginViewModel”的类,然后编写如下代码:因为我使用的userName登录,所以我声明的字段没有Email,而“RememberMe”属性是一个bool 类型,用于让用户选择是否在本地记住账号和密码信息。
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Web; namespace jsdhh2.ViewModels { public class LoginViewModel { [Required] [Display(Name = "用户名")] public string UserName { get; set; } [Required] [DataType(DataType.Password)] [Display(Name = "密码")] public string Password { get; set; } [Display(Name = "记住我?")] public bool RememberMe { get; set; } } }
.5. 编写 Login 方法
在“UserController”控制器中编写一个带有 HttpGet 特性的 Login()方法,用于呈现用户填写登录账户和密码的页面。再编写一个带有 HttpPost 特性的 Login()方法,用于将用户填写的账户和密码提交到服务器进行验证。代码如下:
private SignInManager _signInManager; public SignInManager SignInManager { get { return _signInManager ?? HttpContext.GetOwinContext().Get<SignInManager>(); } private set { _signInManager = value; } }
private ActionResult RedirectToLocal(string returnUrl) { if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } return RedirectToAction("Index", "Home"); } }
SignInManager 属性用于获取当前的应用程序登录管理器,然后就可以使用与登录相关的方法或属性。RedirectToLocal()方法用于完成某个某个操作后进行跳转,如果存在返回的 Url,则跳转到返回 Url,如果没有,则跳转到 Home 控制器的 Index()方法上。
/// <summary> /// 登录 /// </summary> /// <param name="returnUrl"></param> /// <returns></returns> [HttpGet] [AllowAnonymous] public ActionResult Login(string returnUrl) { ViewBag.ReturnUrl = returnUrl; return View(); } /// <summary> /// 登录 /// </summary> /// <param name="model"></param> /// <param name="returnUrl"></param> /// <returns></returns> [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> Login(jsdhh2.ViewModels.LoginViewModel model, string returnUrl) { if (!ModelState.IsValid) { return View(model); } // 这不会计入到为执行帐户锁定而统计的登录失败次数中 // 若要在多次输入错误密码的情况下触发帐户锁定,请更改为 shouldLockout: true var result = await SignInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, shouldLockout: false); switch (result) { case SignInStatus.Success: return RedirectToLocal(returnUrl); case SignInStatus.LockedOut: return View("Lockout"); case SignInStatus.RequiresVerification: return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe }); case SignInStatus.Failure: default: ModelState.AddModelError("", "无效的登录尝试。"); return View(model); } }
同时还有个新方法
private ActionResult RedirectToLocal(string returnUrl) { if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } return RedirectToAction("Index", "Home"); } }
在 HttpPost 的 Login()方法中,如果登录成功,则转到返回 Url,如果用户已锁定,则返回到”Lockout”视图,如果用户未完成验证(邮箱手机验证或双重验证),则转到“SendCode”方法。其它情况,则添加登录失败的错误信息。
6. 编写 Login 视图
“Views”“User”文件夹中添加一个名称为“Login”的视图,并编写如下代码:
@using jsdhh2.Models @model jsdhh2.ViewModels.LoginViewModel @{ ViewBag.Title = "登录"; } <div class="row"> <div class="col-md-8"> <section id="loginForm"> @using (Html.BeginForm("Login", "User", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) { @Html.AntiForgeryToken() <h4>@ViewBag.Title。</h4> <hr /> @Html.ValidationSummary(true, "", new { @class = "text-danger" }) <div class="form-group"> @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" }) <div class="col-md-10"> @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" }) @Html.ValidationMessageFor(m => m.UserName, "", 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="登录" class="btn btn-default" /> </div> </div> <p> @Html.ActionLink("注册为新用户", "Add") </p> @* 在为密码重置功能启用帐户确认后启用此项 <p> @Html.ActionLink("忘记了密码?", "ForgotPassword") </p>*@ } </section> </div> </div>
7.登录样式
输入用户名,秘码登录
图 7-23 上看,是转到了网站的首页,这个首页在“RouteConfig.cs“中默认配置好的。如下代码
namespace Yidosoft.Identity { public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); } } }
在此代码中,配置的默认首页就是 Home 控制器下的 Index()方法,默认的首页地址,在浏览器的地址中,控制器和方法名都是可以省略的。所以图 7-23 转向的地址就是” htttp://localhost:6460/Home/Index”,只是省略了而已。在登录逻辑代码中,转向了 Home/Index,表示用户登录成功了。
8.创建首大页
因为我们是用MVC项目first code,所以home的index视图是自动生成的。
我们主要是编写“_LoginPartial.cshtml“视图。用于呈现用户的登录信息。由于网站的顶部导航视图代码是编写在“Views”\“Shared”文件夹下的“_Layout.cshtml”布局视图中的。所以我们也将“_LoginPartial.cshtml”放在“Views”/“Shared”文件夹下。
@using Microsoft.AspNet.Identity @if (Request.IsAuthenticated) { using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm", @class = "navbar-right" })) { @Html.AntiForgeryToken() <ul class="nav navbar-nav navbar-right"> <li> @Html.ActionLink("你好," + User.Identity.GetUserName() + "!", "Index", "Manage", routeValues: null, htmlAttributes: new { title = "Manage" }) </li> <li><a href="javascript:document.getElementById('logoutForm').submit()">注销</a></li> </ul> } } else { <ul class="nav navbar-nav navbar-right"> <li>@Html.ActionLink("注册", "Register", "Account", routeValues: null, htmlAttributes: new { id = "registerLink" })</li> <li>@Html.ActionLink("登录", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })</li> </ul> }
然后打开“_Layout.cshtml”,将“_LoginPartial.cshtml”以部分视图的形式嵌入。如图7-26 所示:
@Html.Partial("_LoginPartial")
代码,将部分视图嵌入到布局视图中。现在运行一下首页。
的顶部导航栏上,是不是已经看到在前面登录的账户名了,并且还有注销按钮,注销按钮的功能后面会讲解
5. 确认用户
确认用户是指当用户在网站中注册成功之后,会通过邮件或手机发送一个验证码,然后通过这个验证码来确认用户的正确性。这里主要讲解使用电子邮件来确认用户,使用 163 邮箱的网关来发送确认用户的
电子邮件。
5.1. 配置 EmailService
配置 EmailService 服务,需要继承 IidentityMessageService 接口,实现 SendAsync()方法来发送邮件。在“App_Start”文件夹下的“IdentityConfig.cs”文件中的“Yidosoft.Identity”名称空间
下编写如下代码:
public class EmailService : IIdentityMessageService { public Task SendAsync(IdentityMessage message) { //编写发送邮件的代码 Common.SendMail("smtp.126.com", 25, "lbj$4561200", "yidosoft@126.com", message.Destination, message.Subject, message.Body); return Task.FromResult(0); } }
在 SendAsync()方法中就可以编写发送电子邮件的代码。这里使用了 Common 类中的静态方法 SendMail()来发送邮件,使用的是 smtp.126.com 来发送邮件。在“Yidosoft.Identity”的根目录下添加一个 Common 类文件,然后编写如下代码
using System.Net; using System.Net.Mail; namespace jsdhh2 { public class Common { public static bool SendMail(string strSmtpServer, int iSmtpPort, string Password, string strFrom, string strto, string strSubject, string strBody) { //设置发件人信箱,及显示名字 MailAddress mailFrom = new MailAddress(strFrom); //设置收件人信箱,及显示名字 MailAddress mailTo = new MailAddress(strto); //创建一个MailMessage对象 MailMessage oMail = new MailMessage(mailFrom, mailTo); oMail.Subject = strSubject; oMail.Body = strBody; oMail.IsBodyHtml = true; //指定邮件格式,支持HTML格式 oMail.BodyEncoding = System.Text.Encoding.GetEncoding("GB2312");//邮件采用的编码 oMail.SubjectEncoding = System.Text.Encoding.GetEncoding("GB2312");//邮件采用的编码 oMail.Priority = MailPriority.High;//设置邮件的优先级为高 //发送邮件服务器 SmtpClient client = new SmtpClient(); //发送邮件服务器的smtp //每种邮箱都不一致 client.Host = strSmtpServer; //指定邮件服务器 //发送邮件服务器端口 client.Port = iSmtpPort; //设置超时时间 client.Timeout = 9999; //设置为发送认证消息 client.UseDefaultCredentials = true; //指定服务器邮件,及密码 //发邮件人的邮箱地址和密码 client.Credentials = new NetworkCredential(strFrom, Password); client.Send(oMail); //发送邮件 //释放资源 mailFrom = null; mailTo = null; client.Dispose();//释放资源 oMail.Dispose(); //释放资源 return true; } } }
然后在IdentityConfig.CS中的“UserManager”类中的“Create()”方法中添加如下代码:
/// <summary> /// 配置用户管理器 /// </summary> public class UserManager : UserManager<User> { public UserManager(IUserStore<User> store) : base(store) { } public static UserManager Create(IdentityFactoryOptions<UserManager> options, IOwinContext context) { var manager = new UserManager(new UserStore<User>(context.Get<Models.IdentityDbContext>())); manager.EmailService = new EmailService(); var dataProtectionProvider = options.DataProtectionProvider; if (dataProtectionProvider != null) { manager.UserTokenProvider = new DataProtectorTokenProvider<User>(dataProtectionProvider.Create("EmailConfirmation")); } return manager; } }
.5.2. 发送确认用户邮件
带 HttpPost 的 Add()方法中添加发送确认用户的邮件,表示在用户注册后需要验证一下。
打开“UserController.cs”文件,找到带有 HttpPost 特性的 Add()方法。原来的代码如下:
HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> Add(AddUserViewModel addUserModel) { if (ModelState.IsValid) { var user = new User { UserName = addUserModel.Email, Email = addUserModel.Email, QQNumber = addUserModel.QQNumber, WechatNumber = addUserModel.WechatNumber }; var result = await UserManager.CreateAsync(user, addUserModel.Password); if (result.Succeeded) { return RedirectToAction("List", "User"); } AddErrors(result); } return View(addUserModel); }
现在修改一下,添加用户登录和发送确认用户电子邮件的代码,如下:
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> Add(AddUserViewModel addUserModel) { if (ModelState.IsValid) { var user = new User { UserName = addUserModel.Email, Email = addUserModel.Email, QQNumber = addUserModel.QQNumber, WechatNumber = addUserModel.WechatNumber }; var result = await UserManager.CreateAsync(user, addUserModel.Password); if (result.Succeeded) { //登录 await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false); // 发送包含此链接的电子邮件 string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id); var callbackUrl = Url.Action("ConfirmEmail", "User", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme); await UserManager.SendEmailAsync(user.Id, "确认你的帐户", "请通过单击 <a href=\"" + callbackUrl + "\">这里</a>来确认你的帐户"); //return RedirectToAction("List", "User"); return RedirectToAction("Index", "Home"); } AddErrors(result); } return View(addUserModel); }
在此代码中,使用 UserManager.GenerateEmailConfirmationTokenAsync()方法,根据 User.Id 一成一个 Code,然后附加到验证 Url 中。最后使用 UserManager.SendEmailAsync()来发送电子邮件。
5.3. 接收电子邮件:在这要说明,我这没有成功,是因为一是我使用的USERNAME,而非Email,同时我未正确配置邮箱端口。
现在新注册一个用户,使用真实的电子邮件,然后接收该电子邮件,并完成邮件验证。
先按真实的EMAIL地址注册,并登录。再到邮箱中去查看是否有邮箱。
然后登录邮箱
正确转到首页了,并且显示了当前注册的用户信息,表示登录成功了。现在登录注册的邮件,看是否能接收到邮件。如图 7-30 所示:
中已经接收到邮件了,点击“这里”链接去确认账户。如图 7-31 所示
由于我们还没有编写邮件验证的代码,所以图 7-31 的页面是不存在的
.5.4. 邮箱验证
在“UserController”控制器中添加一个名称为“ConfirmEmail()”的方法。代码如下:
/// <summary> /// 邮箱验证 /// </summary> /// <param name="userId"></param> /// <param name="code"></param> /// <returns></returns> [HttpGet] [AllowAnonymous] public async Task<ActionResult> ConfirmEmail(string userId, string code) { if (userId == null || code == null) { return View("Error"); } var result = await UserManager.ConfirmEmailAsync(userId, code); return View(result.Succeeded ? "ConfirmEmail" : "Error"); }
ConfirmEmail()方法需要接收到 2 个参数,1 个是 userId,1 个是 code,与图 7-31浏览器中的 Url 参数是相对应的。
在数据库中存储的用户信息表是 AspNetUsers,有一个字段是“EmailConfirmed”,是 bit 类型,未验证时,该字段的值为 false,验证通过后,修改后 true。如图 7-32
在 ConfirmEmail()方法中用到了“Error”和“ConfirmEmail”这两个视图,在“Views”“User”中添加“ConfirmEmail.cshtml”视图,并编写如下代码:
@{ ViewBag.Title = "确认电子邮件"; } <h2>@ViewBag.Title。</h2> <div> <p> 感谢你确认电子邮件。请 @Html.ActionLink("单击此处登录", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" }) </p> </div>
在“Shared”文件夹中添加“Error.cshtml”视图,并编写如下代码:这个基本是自带的
@model System.Web.Mvc.HandleErrorInfo @{ ViewBag.Title = "Error"; } <h1 class="text-danger">Error.</h1> <h2 class="text-danger">An error occurred while processing your request.</h2>
好了,现在重新运行图 7-31 的页面,如图 7-33 所示
图 7-33 提示已经确认了电子邮件,可以正常登录了。那么我们回到数据库看一下,如图 7-34 所示:
在图 7-34 中,“EmailConfirmed”字段的值已经变为“True”了,表示该用户的电子邮
件已经验证过了