注:本文系作者原创,但可随意转载。
一个星期前,也就是3月20日,微软发布了Asp.Net Identity 2.0 RTM。功能更加强大,也更加稳定。Identity这个东西现在版本还比较低,每次发布新版本都会有较多改动。
2.0新增了很多功能,比如
“双重认证(TFA)" --就是类似密保登陆的功能
”账号锁定”--可以设置账号在短时间内登陆失败达到一定次数则在几分钟内被禁止登陆
“账号认证”--即现在普遍的登陆模式,用户名即邮箱,注册后需要认证才可以登陆
“密码找回”--这个常用功能以前一直未被集成到Identity中
“单点登出(SSO)”--也就是同时打开了几个页面,在任何页面退出,都会导致其他页面的TOKEN失效,从而不能进行账户下的操作
“主键变更”--在1.0版本中用户表的主键是string类型的用户名,现在可以设置任意类型的主键,如int,Guid 等
“集合查询”-- 支持以集合的方式查询Users和Roles表
“删除账号”--现在这个功能可以使用UserManager管理类来实现,过去只能通过dbContext直接操作数据表是很麻烦的
“增强密码规则”--在注册时,可以设置密码规则,包括位数,是否必须大写字母,是否必须小写字母,是否必须数字,是否必须特殊符号等,规则更加强大
其他更多功能请参考官方博客http://blogs.msdn.com/b/webdev/archive/2014/03/20/test-announcing-rtm-of-asp-net-identity-2-0-0.aspx
========================================================================================
当然,上面的功能具体如何实现还需要我们来编写具体的代码,比如实现第三方登陆等,Identity只是给出了基本的脚手架。
下面我基于Identity 2.0新的脚手架来实现他的 注册账号认证 功能,即使用邮箱注册后,系统发送邮件至用户邮箱,用户打开邮箱点击超链接激活账号后才可以登陆。
首先,拿到新东西当然是看文档,然后下载Sample看下基本功能是如何实现的。
官方给出的Sample的安装方法,使用VS 打开菜单 “工具”--》“NuGet程序包管理器"--》”程序包管理器控制台“,打开后输出”Install-Package Microsoft.AspNet.Identity.Samples -Version 2.0.0-beta2 –Pre“ 。然后程序会自动为你安装Sample程序,期间假如你的其他nuget包版本过低,或有重复的文件,会提示你更新等,你可能需要输入Y并按回车。另外需要注意的是:这个SAMPLE包需要安装在一个Empty的MVC项目中。
2.0的脚手架内容和组织架构比以前更复杂,基本的内容您可以查看样板程序,此处不再赘述。下面仅讲解实现邮箱认证功能需要进行的改动。
提示:此示例需要您对Identity有基本的了解,并配合Identity 2.0的Sample Code阅读效果更佳。
========================================================================================
1.配置smtp服务
要发送邮件给注册用户,首先我们需要有个发件邮箱,这里可以随便弄个QQ邮箱之类的。配置的内容可以直接写死在代码里,但为了方便配置,我们把它写到Web.Config中,
1 <configSections> 2 ... 3 <sectionGroup name="application"> 4 ... 5 <section name="mail" type="DotNetRocks.Web.Configurations.MailConfig" allowLocation="true" 6 allowDefinition="Everywhere" requirePermission="false" /> 7 ... 8 </sectionGroup> 9 ... 10 </configSections> 11 ... 12 <application> 13 ... 14 <!-- 测试时可将requireValid 设为false 则不进行邮箱验证--> 15 <mail requireValid="true" server="smtp.qq.com" port="25" uid="something@qq.com" pwd="yourpassword" enableSSL="false" enablePwdCheck="false" /> 16 ... 17 </application> 18 ...
上面的代码中,为了自定义配置节点,需要在configSections节点中,声明我们自定义的节点,这里我们自定义了一组节点叫application。然后我们就可以在下文中详细配置application节点组,其中的mail节点就是和smtp相关的邮箱配置。其中最主要的属性就是smtp服务器地址,端口号(一般默认25),和你的邮箱账号和密码。其他内容可以根据你的需要的策略自行配置。
配置写好了,在程序中要使用时,只需读出其中的数据即可。我们需要将配置读到一个模型中,因此新建一个"Configurations"文件夹,并新建一个"MailConfig"类让它继承ConfigurationSection类,代码如下。
1 public class MailConfig : ConfigurationSection 2 { 3 /// <summary> 4 /// 注册时是否需要验证邮箱 5 /// </summary> 6 [ConfigurationProperty("requireValid", DefaultValue = "false", IsRequired = true)] 7 public bool RequireValid 8 { 9 get 10 { 11 return (bool)this["requireValid"]; 12 } 13 set 14 { 15 this["requireValid"] = value; 16 } 17 } 18 /// <summary> 19 /// SMTP服务器 20 /// </summary> 21 [ConfigurationProperty("server", IsRequired = true)] 22 public string Server 23 { 24 get 25 { 26 return (string)this["server"]; 27 } 28 set 29 { 30 this["server"] = value; 31 } 32 } 33 /// <summary> 34 /// 默认端口25(设为-1让系统自动设置) 35 /// </summary> 36 [ConfigurationProperty("port", DefaultValue = "25", IsRequired = true)] 37 public int Port 38 { 39 get 40 { 41 return (int)this["port"]; 42 } 43 set 44 { 45 this["port"] = value; 46 } 47 } 48 /// <summary> 49 /// 账号 50 /// </summary> 51 [ConfigurationProperty("uid", IsRequired = true)] 52 public string Uid 53 { 54 get 55 { 56 return (string)this["uid"]; 57 } 58 set 59 { 60 this["uid"] = value; 61 } 62 } 63 /// <summary> 64 /// 密码 65 /// </summary> 66 [ConfigurationProperty("pwd", IsRequired = true)] 67 public string Pwd 68 { 69 get 70 { 71 return (string)this["pwd"]; 72 } 73 set 74 { 75 this["pwd"] = value; 76 } 77 } 78 /// <summary> 79 /// 是否使用SSL连接 80 /// </summary> 81 [ConfigurationProperty("enableSSL", DefaultValue = "false", IsRequired = false)] 82 public bool EnableSSL 83 { 84 get 85 { 86 return (bool)this["enableSSL"]; 87 } 88 set 89 { 90 this["enableSSL"] = value; 91 } 92 } 93 /// <summary> 94 /// 95 /// </summary> 96 [ConfigurationProperty("enablePwdCheck", DefaultValue = "false", IsRequired = false)] 97 public bool EnablePwdCheck 98 { 99 get 100 { 101 return (bool)this["enablePwdCheck"]; 102 } 103 set 104 { 105 this["enablePwdCheck"] = value; 106 } 107 }
在使用时,只需要MailConfig config = (MailConfig)ConfigurationManager.GetSection("application/mail"); 即可读取到配置属性,更详细的内容可以在ConfigurtaionSection上按F1参考MSDN。
2.配置UserManager
在项目脚手架中,App_Start文件夹下有个IdentityConfig.cs文件,打开他,其中有一个继承了UserManager<ApplicationUser>的ApplicationUserManage类。(如果你没有变更文件结构的话,当然你可以根据需求自行调整整个文件组织结构)。
在ApplicationUserManager中可以配置的东西很多,比如账号锁定规则,密码强度规则,密保登陆等。其中还有一句manager.EmailService = new EmailService();, 而这个EmailService类就在本文件中,他继承了IIdentityMessageService接口,这个接口总共只有一个方法SendAsync,也就是发送邮件,我们只需要在这个方法中实现发送邮件的逻辑,在需要发送邮件时UserManager会自动调用该方法。
1 /// <summary> 2 /// 邮箱验证Service 3 /// </summary> 4 public class EmailService : IIdentityMessageService 5 { 6 public async Task SendAsync(IdentityMessage message) 7 { 8 MailConfig mailConfig = (MailConfig)ConfigurationManager.GetSection("application/mail"); 9 if (mailConfig.RequireValid) 10 { 11 // 设置邮件内容 12 var mail = new MailMessage( 13 new MailAddress(mailConfig.Uid, "no-reply"), 14 new MailAddress(message.Destination) 15 ); 16 mail.Subject = message.Subject; 17 mail.Body = message.Body; 18 mail.IsBodyHtml = true; 19 mail.BodyEncoding = Encoding.UTF8; 20 // 设置SMTP服务器 21 var smtp = new SmtpClient(mailConfig.Server, mailConfig.Port); 22 smtp.Credentials = new System.Net.NetworkCredential(mailConfig.Uid, mailConfig.Pwd); 23 24 await smtp.SendMailAsync(mail); 25 } 26 await Task.FromResult(0); 27 } 28 }
上面这段代码简单的实现了邮件发送逻辑,当然也可以有更复杂的策略,比如是否使用SSL连接等此处我并未配置。关于电子邮件的相关知识可以参考:http://systemnetmail.com/
代码写到这里,运行程序,尝试注册新用户,如果邮箱配置没有问题的话,新用户应该已经可以收到系统发来的邮件了。不过现在即使未验证邮箱也可以登陆,需要自己实现未验证邮箱禁止登陆的功能。
3.变更Login策略
首先说一下,在注册功能中,为了方便测试,注册后跳转的页面直接提供了激活邮箱的连接,正式运行的话需要把它删除,我们只需要把该连接发送到用户邮箱即可。
在登陆时,统一调用的是SingInHelper中的PasswordSignIn方法,然后返回枚举类型的SignInStatus,来决定将哪个页面返回给用户。现在SignInStatus中并没有邮箱未验证禁止登陆的状态。因此我们要在其中加一个,比如叫”InvalidEmail"。如果返回的是SignInStatus.InvalidEmail,则让用户跳转到提示邮箱未激活的界面。也就是在Login方法中的switch语句中加一个case,如下图
1 public async Task<ActionResult> Login(LoginViewModel model, string returnUrl){ 2 //...此处省略若干代码 3 switch (result) 4 { 5 case SignInStatus.Success: 6 return RedirectToLocal(returnUrl); 7 case SignInStatus.LockedOut: 8 return View("Lockout"); 9 case SignInStatus.InvalidEmail: 10 return View("DisplayEmail"); 11 case SignInStatus.RequiresTwoFactorAuthentication: 12 return RedirectToAction("SendCode", new { ReturnUrl = returnUrl }); 13 case SignInStatus.Failure: 14 default: 15 ModelState.AddModelError("", "Invalid login attempt."); 16 return View(model); 17 } 18 }
这里,我直接跳转到DisplayEmail页面,提示用户未激活邮箱,禁止登陆,并询问他是否需要再次发送验证邮件。当然这个页面我自己做了需要的修改。
然后我们需要在PasswordSignIn方法中也加入相应的策略以使它能够返回InvalidEmail状态值。
1 public async Task<SignInStatus> PasswordSignIn(string userName, string password, bool isPersistent, bool shouldLockout) 2 { 3 var user = await UserManager.FindByNameAsync(userName); 4 if (user == null) 5 { 6 return SignInStatus.Failure; 7 } 8 if (await UserManager.IsLockedOutAsync(user.Id)) 9 { 10 return SignInStatus.LockedOut; 11 } 12 if (!await UserManager.IsEmailConfirmedAsync(user.Id)) 13 { 14 return SignInStatus.InvalidEmail; 15 } 16 if (await UserManager.CheckPasswordAsync(user, password)) 17 { 18 return await SignInOrTwoFactor(user, isPersistent); 19 } 20 if (shouldLockout) 21 { 22 // If lockout is requested, increment access failed count which might lock out the user 23 await UserManager.AccessFailedAsync(user.Id); 24 if (await UserManager.IsLockedOutAsync(user.Id)) 25 { 26 return SignInStatus.LockedOut; 27 } 28 } 29 return SignInStatus.Failure; 30 }
在上面的代码中,未加邮箱验证前,用户登录后的逻辑顺序,1.判断是否存在该账号,2.判断该账号是否锁定,3.检测账号密码是否正确(若正确直接登陆,否则失败次数+1),4(若走到这一步则说明账号密码错误)检测是否需要锁定账号,5返回登陆失败。根据上面的逻辑,我们应该把邮箱验证加到2和3之间,也就是如上图代码中调用UserManager的IsEmailConfirmedAsync方法,来验证用户邮箱是否认证。
至此,整个功能应该已经全部实现了。
================================================================================
此文是在我已经实现后第二天所写,若步骤有遗漏或错误,欢迎指正补充