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”了,表示该用户的电子邮
件已经验证过了
浙公网安备 33010602011771号