AspNetCore-MVC实战系列(二)之通过绑定邮箱找回密码
AspNetCore - MVC实战系列目录
. 爱留图网站诞生
. git源码:https://github.com/shenniubuxing3/LovePicture.Web
. AspNetCore - MVC实战系列(一)之Sqlserver表映射实体模型
. AspNetCore-MVC实战系列(二)之通过绑定邮箱找回密码
开篇唠嗑
本篇内容写在5.1假期前夕,主要是让大家能在节假日休息充点的时候能有好的干货例子,到目前为止netcore方面的实战例子分享即将进入正轨,谢谢各位朋友多多支持;最近工作安排的新项目即将开始,项目前期就我一人搭建,让我犹豫的是对于公司这个内部系统并且是初建的项目用什么开发方式好呢,最初考虑的是mvc5但是又想了下如果这样还不如直接使用NetCore1.1的MVC呢,因为现在这版本基本也算稳定了可以试试水,可是又有顾虑是mvc项目在上线的时候会影响到其他人的使用(前期不考虑nginx分发),然后目光又转向aspx网站的方式,不得不说这种方式在发布上的确有优势,尤其是在没有分布式的前提下;好吧目前还在考虑中,希望能得到各位朋友的建议。。。
注册模块
首先,这里讲解的内容对应的实体和表结构是基于上一篇文章创建的项目这里就不多说了;对于一个注册功能来说,通常需要的属性是:账号,密码,确认密码,验证码(可省略),甚至有些快捷的注册方式就是通过手机号来注册,当然咋们没有短信通道的功能不能发短信,所以采用前者,先来看下Action中设计代码:
Register的get路由Action
1 // GET: Member/Create 2 public IActionResult Register() 3 { 4 return View(); 5 }
Register的post提交注册信息的Action
1 [HttpPost] 2 [ValidateAntiForgeryToken] 3 public async Task<IActionResult> Register([Bind("UserName,UserPwd,ComfirmPwd")] MoRegisterUser loginUser) 4 { 5 if (ModelState.IsValid) 6 { 7 #region 验证 8 if (_context.ToUserInfo.Any(b => b.UserName.ToUpper() == loginUser.UserName.Trim().ToUpper())) 9 { 10 this.MsgBox("已经存在相同的账号!"); 11 return View(loginUser); 12 } 13 #endregion 14 15 #region 入库 16 17 ToUserInfo userInfo = new ToUserInfo(); 18 19 userInfo.UserName = loginUser.UserName.Trim(); 20 userInfo.UserPwd = PublicClass._Md5(loginUser.UserPwd.Trim()); 21 userInfo.NickName = userInfo.UserName; 22 userInfo.Status = (int)EnumHelper.EmUserStatus.启用; 23 userInfo.CreateTime = DateTime.Now; 24 userInfo.LevelNum = (int)EmLevelNum.注册; 25 26 userInfo.Ips = this.GetUserIp(); 27 userInfo.HeadPhoto = "/images/ailiutu_user.png"; 28 userInfo.Sex = false; 29 30 _context.Add(userInfo); 31 var result = await _context.SaveChangesAsync(); 32 if (result > 0) 33 { 34 var moUserInfo = new MoUserInfo 35 { 36 Id = userInfo.Id, 37 UserName = userInfo.UserName, 38 NickName = userInfo.NickName, 39 Addr = userInfo.Addr, 40 Birthday = userInfo.Birthday, 41 42 Blog = userInfo.Blog, 43 CreateTime = userInfo.CreateTime, 44 Email = userInfo.Email, 45 HeadPhoto = userInfo.HeadPhoto, 46 Introduce = userInfo.Introduce, 47 48 Ips = userInfo.Ips, 49 LevelNum = userInfo.LevelNum, 50 Sex = userInfo.Sex, 51 Tel = userInfo.Tel, 52 Status = userInfo.Status, 53 54 LoginTime = DateTime.Now 55 }; 56 HttpContext.Session.Set<MoUserInfo>(HttpContext.Session.SessionKey(), moUserInfo); 57 58 if (!string.IsNullOrWhiteSpace(moUserInfo.Ips)) 59 { 60 _context.ToUserLog.Add(new ToUserLog 61 { 62 CodeId = (int)EmLogCode.登录, 63 CreateTime = DateTime.Now, 64 Des = $"IP:{moUserInfo.Ips},登录时间:{moUserInfo.LoginTime.ToString("yyyy-MM-dd HH:mm")}", 65 UserId = userInfo.Id 66 }); 67 } 68 69 _context.ToUserLog.Add(new ToUserLog 70 { 71 CodeId = (int)EmLogCode.积分, 72 CreateTime = DateTime.Now, 73 Des = $"【注册】 +{(int)EmLevelNum.注册}", 74 UserId = userInfo.Id 75 }); 76 await _context.SaveChangesAsync(); 77 78 return RedirectToAction(nameof(HomeController.Index), "home"); 79 } 80 #endregion 81 82 this.MsgBox("注册失败,请稍后重试。"); 83 return View(loginUser); 84 } 85 return View(loginUser); 86 }
通过Post的Action能够看出注册处理的Action主要操作步骤有以下几点:
. ModelState.IsValid验证提交的信息是否满足model规则设置
. Linq的Any()方法验证是否存在相同账号
. _context.Add()入库注册信息
. HttpContext.Session.Set的扩展方法设置登陆的session
. 记录登陆记录和登陆增加的积分信息
对于一个简单的注册基本就是这样的流程,我们来看看提交注册时的模型实体:
1 /// <summary> 2 /// 注册实体 3 /// </summary> 4 public class MoRegisterUser 5 { 6 [Required(AllowEmptyStrings = false, ErrorMessage = "账号长度范围6-30字符!")] 7 [Display(Prompt = "邮箱/手机号/6-30字符")] 8 [RegularExpression(@"[^\s]{6,30}", ErrorMessage = "账号长度范围6-30字符。")] 9 public string UserName { get; set; } 10 11 [Required(AllowEmptyStrings = false, ErrorMessage = "密码长度范围6-20字符!")] 12 [DataType(DataType.Password)] 13 [Display(Prompt = "密码长度范围6-20字符!")] 14 [RegularExpression(@"[^\s]{6,20}", ErrorMessage = "密码长度范围6-20字符。")] 15 public string UserPwd { get; set; } 16 17 [Compare("UserPwd", ErrorMessage = "密码与确认密码不相同!")] 18 [DataType(DataType.Password)] 19 [Display(Prompt = "必须与密码相同")] 20 public string ComfirmPwd { get; set; } 21 }
这里自定义的注册模型,设置了DataAnnotations,以此来快速设置验证,不用再每个都用js写了,mvc框架帮你做了这些;下面看看View的代码:
1 @model LovePicture.Model.MoClass.MoRegisterUser 2 3 @{ 4 ViewData["Title"] = "注册"; 5 } 6 7 <h3><span class="glyphicon glyphicon-flag" aria-hidden="true"></span> 注册</h3> 8 <form name="form_submit" asp-action="Register"> 9 <div class="form-horizontal"> 10 <h4> 爱留图:欢迎您成为我们的一份子,让我们一起留存珍惜的图片吧。</h4> 11 <hr /> 12 <div asp-validation-summary="ModelOnly" class="text-danger"></div> 13 <div class="form-group"> 14 <label asp-for="UserName" class="col-md-2 control-label">账号</label> 15 <div class="col-md-10"> 16 <input asp-for="UserName" required="required" class="form-control" /> 17 <span asp-validation-for="UserName" class="text-danger"></span> 18 </div> 19 </div> 20 <div class="form-group"> 21 <label asp-for="UserPwd" class="col-md-2 control-label">密码</label> 22 <div class="col-md-10"> 23 <input asp-for="UserPwd" required="required" class="form-control" /> 24 <span asp-validation-for="UserPwd" class="text-danger"></span> 25 </div> 26 </div> 27 <div class="form-group"> 28 <label asp-for="ComfirmPwd" class="col-md-2 control-label">确认密码</label> 29 <div class="col-md-10"> 30 <input asp-for="ComfirmPwd" class="form-control" /> 31 <span asp-validation-for="ComfirmPwd" class="text-danger"></span> 32 </div> 33 </div> 34 35 <div class="form-group"> 36 <div class="col-md-offset-2 col-md-10"> 37 <input type="button" value="注 册" name="btnSubmit" class="btn btn-default" /> 38 <span id="msgbox" style="color:red">@ViewData["msgbox"]</span> 39 </div> 40 </div> 41 </div> 42 </form> 43 <br /> 44 <div> 45 <a href="/member/login">有账号去登录</a> | <a href="/member/ForgetPassword">忘记密码?</a> 46 </div> 47 <script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script> 48 <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
对于mvc模型注解的方式在前端需要引入这两个js文件
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script> <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
View内容注意点在于我试图中的Button按钮是不是submit形式,这样做的理由是,当您注册的Action业务过多时,用户点击注册按钮提交数据,这个时候如果无法快速响应信息给用户,那么用户可能多次点击,因此就有个需求是需要吧注册提交按钮置灰或者影藏点,这里我为了方便把全站点的提交按钮都弄成统一name的按钮了,最后用js来提交submit(注册效果):
1 bindSubmitBtn: function () { 2 $("input[name='btnSubmit']").on("click", function () { 3 4 var _btn = $(this); 5 _btn.addClass("hide"); 6 var _msg = $("#msgbox"); 7 _msg.html("提交中,请稍后..."); 8 9 var _form = $("form[name='form_submit']"); 10 if (_form.valid()) { 11 _form.submit(); 12 } else { 13 _btn.removeClass("hide"); 14 _msg.html(""); 15 } 16 }); 17 }
登录模块
从代码上来说登录和注册相差不大,功能上登录模块主要用来验证登陆用户是否存在,分配唯一sessionid,如果有跳转地址还需要在登陆成功后执行跳回原访问地址;
Login的Get方式Action
1 // GET: Member 2 public IActionResult Login(string returnUrl = null) 3 { 4 //获取session 5 var userInfo = HttpContext.Session.Get<MoUserInfo>(HttpContext.Session.SessionKey()); 6 if (userInfo != null) 7 { 8 if (string.IsNullOrWhiteSpace(returnUrl)) { return RedirectToAction(nameof(HomeController.Index), "Home"); } 9 else { Redirect(returnUrl); } 10 } 11 this.MsgBox(returnUrl, "returnUrl"); 12 return View(); 13 }
Login的Post登录方式Action
1 [HttpPost] 2 [ValidateAntiForgeryToken] 3 public async Task<IActionResult> Login([Bind("UserName,UserPwd,ReturnUrl")] MoLoginUser loginUser) 4 { 5 if (ModelState.IsValid) 6 { 7 #region 验证 8 var md5Pwd = PublicClass._Md5(loginUser.UserPwd.Trim()); 9 var userInfo = await _context.ToUserInfo.SingleOrDefaultAsync(b => 10 b.UserName.Equals(loginUser.UserName, StringComparison.CurrentCultureIgnoreCase) && 11 b.UserPwd.Equals(md5Pwd)); 12 if (userInfo == null) 13 { 14 this.MsgBox("账号或密码错误!"); 15 return View(loginUser); 16 } 17 else if (userInfo.Status == (int)EnumHelper.EmUserStatus.禁用) 18 { 19 this.MsgBox("该账号已被禁用,或许你可以尝试重新注册一个账号!"); 20 return View(loginUser); 21 } 22 #endregion 23 24 #region 更新登录信息 25 userInfo.Ips = this.GetUserIp(); 26 userInfo.LoginTime = DateTime.Now; 27 userInfo.LevelNum += (int)EmLevelNum.登录; 28 29 //记录session 30 var moUserInfo = new MoUserInfo 31 { 32 Id = userInfo.Id, 33 UserName = userInfo.UserName, 34 NickName = userInfo.NickName, 35 Addr = userInfo.Addr, 36 Birthday = userInfo.Birthday, 37 38 Blog = userInfo.Blog, 39 CreateTime = userInfo.CreateTime, 40 Email = userInfo.Email, 41 HeadPhoto = userInfo.HeadPhoto, 42 Introduce = userInfo.Introduce, 43 44 Ips = userInfo.Ips, 45 LevelNum = userInfo.LevelNum, 46 Sex = userInfo.Sex, 47 Tel = userInfo.Tel, 48 Status = userInfo.Status, 49 50 LoginTime = Convert.ToDateTime(userInfo.LoginTime) 51 }; 52 HttpContext.Session.Set<MoUserInfo>(HttpContext.Session.SessionKey(), moUserInfo); 53 54 if (!string.IsNullOrWhiteSpace(moUserInfo.Ips)) 55 { 56 _context.ToUserLog.Add(new ToUserLog 57 { 58 CodeId = (int)EmLogCode.登录, 59 CreateTime = DateTime.Now, 60 Des = $"IP:{moUserInfo.Ips},登录时间:{moUserInfo.LoginTime.ToString("yyyy-MM-dd HH:mm")}", 61 UserId = userInfo.Id 62 }); 63 } 64 65 _context.ToUserLog.Add(new ToUserLog 66 { 67 CodeId = (int)EmLogCode.积分, 68 CreateTime = DateTime.Now, 69 Des = $"【登录】 +{(int)EmLevelNum.登录}", 70 UserId = userInfo.Id 71 }); 72 73 await _context.SaveChangesAsync(); 74 75 if (string.IsNullOrWhiteSpace(loginUser.ReturnUrl)) { return RedirectToAction(nameof(HomeController.Index), "Home"); } 76 else { return Redirect(loginUser.ReturnUrl); } 77 #endregion 78 } 79 return View(loginUser); 80 }
这里模仿微软官方实例给出的样子,把登陆和注册实体模型分开了,因为登陆模型不需要什么重复密码,并且还有其他的属性如:回调地址,验证码等:
1 /// <summary> 2 /// 登录实体 3 /// </summary> 4 public class MoLoginUser 5 { 6 [Required(AllowEmptyStrings = false, ErrorMessage = "账号长度范围6-30字符!")] 7 [Display(Prompt = "邮箱/手机号/6-30字符")] 8 [RegularExpression(@"[^\s]{6,30}", ErrorMessage = "账号长度范围6-30字符。")] 9 public string UserName { get; set; } 10 11 [Required(AllowEmptyStrings = false, ErrorMessage = "密码长度范围6-20字符!")] 12 [DataType(DataType.Password)] 13 [Display(Prompt = "密码长度范围6-20字符!")] 14 [RegularExpression(@"[^\s]{6,20}", ErrorMessage = "密码长度范围6-20字符。")] 15 public string UserPwd { get; set; } 16 17 /// <summary> 18 /// 回跳地址 19 /// </summary> 20 public string ReturnUrl { get; set; } 21 }
同理对于登录的view设计也和注册差不多,只是不同网站对于安全设置可能会增加一些验证码,或其他的验证方式而已,如下登录View代码:
1 @model LovePicture.Model.MoClass.MoLoginUser 2 3 @{ 4 ViewData["Title"] = "登录"; 5 } 6 7 <h3><span class="glyphicon glyphicon-send" aria-hidden="true"></span> 登录</h3> 8 <form name="form_submit" asp-action="Login"> 9 <div class="form-horizontal"> 10 <h4>爱留图:即刻登录,让我们一起留存珍惜的图片吧。</h4> 11 <hr /> 12 <div asp-validation-summary="ModelOnly" class="text-danger"></div> 13 <div class="form-group"> 14 <label asp-for="UserName" class="col-md-2 control-label">账号</label> 15 <div class="col-md-10"> 16 <input asp-for="UserName" required="required" class="form-control" /> 17 <span asp-validation-for="UserName" class="text-danger"></span> 18 </div> 19 </div> 20 <div class="form-group"> 21 <label asp-for="UserPwd" class="col-md-2 control-label">密码</label> 22 <div class="col-md-10"> 23 <input asp-for="UserPwd" required="required" class="form-control" /> 24 <span asp-validation-for="UserPwd" class="text-danger"></span> 25 </div> 26 </div> 27 28 <div class="form-group"> 29 <div class="col-md-offset-2 col-md-10"> 30 <input type="button" value="登 录" name="btnSubmit" class="btn btn-default" /> <a href="/member/register" title="没账号来这里注册吧">没账号这里注册</a> 31 <span id="msgbox" style="color:red">@ViewData["msgbox"]</span> 32 </div> 33 </div> 34 </div> 35 <input type="hidden" name="ReturnUrl" value="@ViewData["returnUrl"]" /> 36 </form> 37 <br /> 38 <div> 39 <a href="/member/register">没账号这里注册</a> | <a href="/member/ForgetPassword">忘记密码?</a> 40 </div> 41 <script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script> 42 <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
邮箱找回密码
来到这里才真正进入今天的主题,对于一些有安全设置的网站来说通常有类似于通过绑定邮箱找回密码的功能,这里爱留图目前也做了这样的设置:
如果是绑定邮箱的用户,可以通过找回密码-通过绑定邮箱找回密码的功能来获取重新设置密码的权限;首先我们需要一个忘记密码的界面,好让用户填写自己之前绑定好的邮箱,并且提交发送找回邮箱的邮件操作,因此有了如下Ation代码:
ForgetPassword忘记密码get的Action
1 public IActionResult ForgetPassword() 2 { 3 return View(); 4 }
ForgetPassword忘记密码post提交发送邮件的Action
1 /// <summary> 2 /// 提交忘记密码内容 3 /// </summary> 4 /// <param name="email"></param> 5 /// <returns></returns> 6 [HttpPost] 7 [ValidateAntiForgeryToken] 8 public async Task<IActionResult> ForgetPassword(string email) 9 { 10 if (string.IsNullOrWhiteSpace(email)) { this.MsgBox("邮箱必填!"); return View(); } 11 12 email = email.Trim().ToLower(); 13 if (email.Length >= 50 || email.Length <= 3) 14 { 15 this.MsgBox("邮箱长度不符!"); return View(); 16 } 17 else if (!email.Contains("@")) 18 { 19 this.MsgBox("邮箱格式不正确!"); return View(); 20 } 21 var user = await _context.ToUserInfo.SingleOrDefaultAsync(b => b.Email.ToLower() == email); 22 if (user == null) { this.MsgBox("不存在该绑定邮箱的账号!"); return View(); } 23 else if (user.Status == (int)EnumHelper.EmUserStatus.禁用) 24 { 25 this.MsgBox("该绑定邮箱的账号已被禁用,可以通过发送邮件至:841202396@qq.com联系客服!"); return View(); 26 } 27 28 var timeOut = 10; 29 var now = DateTime.Now.AddMinutes(timeOut); 30 var expires = now.ToString("yyyy-MM-dd HH:mm"); 31 var token = PublicClass._Md5($"{expires}-{email}-{Request.Host.Host}"); 32 var appUrl = $"http://{Request.Host.Host}:{Request.Host.Port}"; 33 var comfirmUrl = $"{appUrl}/member/confirmpassword?expire={expires}&token={token}&email={email}&t=0.{now.ToString("ssfff")}"; 34 35 //读取模板 36 var tpl = await PublicClass._GetHtmlTpl(EnumHelper.EmEmailTpl.MsgBox, _selfSetting.EmailTplPath); 37 if (string.IsNullOrWhiteSpace(tpl)) { this.MsgBox("发送绑定邮件失败,请稍后重试。"); return View(); } 38 39 tpl = tpl.Replace("{name}", "尊敬的用户"). 40 Replace("{content}", $"您正在使用<a href='{appUrl}'>爱留图网</a>邮箱重置密码功能,请点击以下链接确认绑定邮箱<a href='{comfirmUrl}'>{comfirmUrl}</a>;注意该地址有效时间{timeOut}分钟。"); 41 //发送 42 var isOk = PublicClass._SendEmail( 43 new Dictionary<string, string> { 44 { "尊敬的用户",email} 45 }, 46 "爱留图 - 重置密码", 47 tpl); 48 49 this.MsgBox(isOk ? "已给您邮箱发送了重置密码邮件,请收件后点击重置密码链接地址。" : "发送绑定邮件失败,请稍后重试!"); 50 51 return View(); 52 }
这里值得关注的是咋们构造了一个修改密码的确认链接comfirmUrl,这里我简单设置的有链接超时时间,token加密的信息,这些信息主要用来在后面用户点击此链接的时候跳转到重置密码的路由,当然我这里是简单设置验证制作出来的重置密码的url,大的门户网站可不会像这样如此简单构造链接和token的,这里经供参考吧;这里涉及到需要发送邮件,我采用的是netcore第三方组件MailKit(就目前为止个人感觉很好用,能兼容很多邮件的方式),这里我用的是qq邮箱服务器:
1 /// <summary> 2 /// 发送邮件 3 /// </summary> 4 /// <param name="dicToEmail"></param> 5 /// <param name="title"></param> 6 /// <param name="content"></param> 7 /// <param name="name"></param> 8 /// <param name="fromEmail"></param> 9 /// <returns></returns> 10 public static bool _SendEmail( 11 Dictionary<string, string> dicToEmail, 12 string title, string content, 13 string name = "爱留图网", string fromEmail = "841202396@qq.com") 14 { 15 var isOk = false; 16 try 17 { 18 if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(content)) { return isOk; } 19 20 //设置基本信息 21 var message = new MimeMessage(); 22 message.From.Add(new MailboxAddress(name, fromEmail)); 23 foreach (var item in dicToEmail.Keys) 24 { 25 message.To.Add(new MailboxAddress(item, dicToEmail[item])); 26 } 27 message.Subject = title; 28 message.Body = new TextPart("html") 29 { 30 Text = content 31 }; 32 33 //链接发送 34 using (var client = new SmtpClient()) 35 { 36 // For demo-purposes, accept all SSL certificates (in case the server supports STARTTLS) 37 client.ServerCertificateValidationCallback = (s, c, h, e) => true; 38 39 client.Connect("smtp.qq.com", 587, false); //这里是qq邮箱 40 41 // Note: since we don't have an OAuth2 token, disable 42 // the XOAUTH2 authentication mechanism. 43 client.AuthenticationMechanisms.Remove("XOAUTH2"); 44 45 // Note: only needed if the SMTP server requires authentication 46 client.Authenticate("您邮箱", "你的邮箱密码"); 47 48 client.Send(message); 49 client.Disconnect(true); 50 } 51 isOk = true; 52 } 53 catch (Exception ex) 54 { 55 56 } 57 return isOk; 58 }
通过参数 message.Body = new TextPart("html") 来设置允许邮箱发送html,为了我修改邮件模板方便我这里自定义了邮件模板SettingEmail.html,主要是一些样式的设置和内容部分的设置:
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 <title>爱留图 - 绑定邮箱模板</title> 7 </head> 8 <body> 9 <h3>{name} 您好:</h3> 10 <div style='padding-top:20px;width:100%'>{content}</div> 11 <div style='padding-top:30px;width:100%'>温馨提示:如果您不是<a href='http://www.lovexins.com:9527'>爱留图</a>用户,请勿点击。</div> 12 <div style='padding-top:50px;width:100%'> 13 <a href='http://www.lovexins.com:9527'> 14 <img style="width:50px;height:50px" src="http://www.lovexins.com:9527/images/ailiutu_user.png" /> 15 <br /> 16 爱留图网 17 </a> 18 </div> 19 <div style='padding-top:5px;width:100%;color:#ccc;border-top:1px solid #ccc'>此为系统邮件请勿回复</div> 20 </body> 21 </html>
有了html模板,咋们还需要把这个文件读取出来,加入到发送邮件内容中,读取html模板:
1 public static async Task<string> _GetHtmlTpl(EnumHelper.EmEmailTpl tpl, string folderPath = @"D:\F\学习\vs2017\netcore\LovePicture.Web\wwwroot\tpl") 2 { 3 var content = string.Empty; 4 if (string.IsNullOrWhiteSpace(folderPath)) { return content; } 5 6 var path = $"{folderPath}/{tpl}.html"; 7 try 8 { 9 using (var stream = File.OpenRead(path)) 10 { 11 using (var reader = new StreamReader(stream)) 12 { 13 content = await reader.ReadToEndAsync(); 14 } 15 } 16 } 17 catch (Exception ex) 18 { 19 throw new Exception(ex.Message); 20 } 21 return content; 22 }
好的到这里,通过用户输入的绑定邮箱,然后发送邮件的功能就完成了,来看看效果吧:
怎么样是不是感觉瞬间逼格提升了很多呢哈哈,电子邮件的推广和用途很多公司都太小看了,我们公司也是哎,什么时候您公司能够重视并且能够像jd那样常常个你发推送邮件,那么估计你离成功不远了嘿嘿;
接受重置密码通知
来继续咋们的讲解,当用户点击重置密码的链接后,我们需要一个接受的地址,这里我的Action取名为ConfirmPassword,接受的参数和我构造重置密码确认链接时候差不多:
1 /// <summary> 2 /// 接受重置密码通知 3 /// </summary> 4 /// <returns></returns> 5 public IActionResult ConfirmPassword(string expire, string token, string email, string t) 6 { 7 if (string.IsNullOrWhiteSpace(expire) || string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(email) || !email.Contains("@") || string.IsNullOrWhiteSpace(t)) 8 { 9 return RedirectToAction(nameof(HomeController.Error), "home", new { msg = "无效的请求。" }); 10 } 11 else if (t.Length != 7) 12 { 13 return RedirectToAction(nameof(HomeController.Error), "home", new { msg = "无效的请求。" }); 14 } 15 16 email = email.Trim().ToLower(); 17 if (!DateTime.TryParse(expire, out var expires)) { return RedirectToAction(nameof(HomeController.Error), "home", new { msg = "无效的请求!" }); } 18 else if (expires.AddMinutes(30) < DateTime.Now) 19 { 20 return RedirectToAction(nameof(HomeController.Error), "home", new { msg = "请求已过期,重新操作!" }); 21 } 22 23 var compareToken = PublicClass._Md5($"{expire}-{email}-{Request.Host.Host}"); 24 if (!token.Equals(compareToken)) { return RedirectToAction(nameof(HomeController.Error), "home", new { msg = "验证失败,无效的请求!" }); } 25 26 var user = _context.ToUserInfo.SingleOrDefault(b => b.Email.ToLower() == email); 27 if (user == null) { return RedirectToAction(nameof(HomeController.Error), "home", new { msg = "不存在该绑定邮箱的账号!" }); } 28 else if (user.Status == (int)EnumHelper.EmUserStatus.禁用) 29 { 30 return RedirectToAction(nameof(HomeController.Error), "home", new { msg = "该绑定邮箱的账号已被禁用,可以通过发送邮件至:841202396@qq.com联系客服!" }); 31 } 32 33 var key = $"checkConfirmPwd{email}"; 34 if (!_cache.TryGetValue<MoUserInfo>(key, out var result)) 35 { 36 _cache.Set<MoUserInfo>(key, new MoUserInfo { Id = user.Id, Email = email }, TimeSpan.FromMinutes(10)); 37 } 38 39 return View(new MoRegisterUser { UserName = email }); 40 }
通过各种安全性的验证(如:链接过期,不存在此用户,token验证失败等),最终会跳转到填写重置密码界面:
重置密码的界面和提交后的处理方式,基本和登陆,注册差不多,所以这里我直接贴出提交重置密码的Action代码:
1 /// <summary> 2 /// 提交重置的密码 3 /// </summary> 4 /// <param name="user"></param> 5 /// <returns></returns> 6 [HttpPost] 7 [ValidateAntiForgeryToken] 8 public async Task<IActionResult> ConfirmPassword([Bind("UserName", "UserPwd", "ComfirmPwd")]MoRegisterUser registUser) 9 { 10 if (ModelState.IsValid) 11 { 12 if (string.IsNullOrWhiteSpace(registUser.UserPwd)) 13 { 14 this.MsgBox("密码不能为空!"); 15 return View(registUser); 16 } 17 else if (string.IsNullOrWhiteSpace(registUser.ComfirmPwd)) 18 { 19 this.MsgBox("确认密码不能为空!"); 20 return View(registUser); 21 } 22 else if (registUser.UserPwd != registUser.ComfirmPwd) 23 { 24 this.MsgBox("密码和确认密码不相同!"); 25 return View(registUser); 26 } 27 28 var key = $"checkConfirmPwd{registUser.UserName}"; 29 if (!_cache.TryGetValue<MoUserInfo>(key, out var checkUser)) 30 { 31 return RedirectToAction(nameof(HomeController.Error), "home", new { msg = "请求已过期,重新操作!" }); 32 } 33 34 var user = _context.ToUserInfo.Where(b => b.Id == checkUser.Id && b.Email == checkUser.Email).SingleOrDefault(); 35 if (user == null) 36 { 37 _cache.Remove(key); 38 return RedirectToAction(nameof(HomeController.Error), "home", new { msg = "重置的密码失败,请稍后重试!" }); 39 } 40 41 user.UserPwd = PublicClass._Md5(registUser.UserPwd.Trim()); 42 var result = await _context.SaveChangesAsync(); 43 if (result > 0) 44 { 45 _cache.Remove(key); 46 this.MsgBox("重置密码成功!"); 47 } 48 else { this.MsgBox("重置密码失败!"); } 49 } 50 return View(registUser); 51 }
下面是确认密码界面的View设计
1 @model LovePicture.Model.MoClass.MoRegisterUser 2 3 @{ 4 ViewData["Title"] = "重置密码"; 5 } 6 7 <h3><span class="glyphicon glyphicon-flag" aria-hidden="true"></span> 重置密码</h3> 8 <form name="form_submit" asp-action="ConfirmPassword"> 9 <div class="form-horizontal"> 10 <h4> 爱留图:欢迎您成为我们的一份子,让我们一起留存珍惜的图片吧。</h4> 11 <hr /> 12 <input type="hidden" name="UserName" value="@Model.UserName"/> 13 <div asp-validation-summary="ModelOnly" class="text-danger"></div> 14 <div class="form-group"> 15 <label asp-for="UserPwd" class="col-md-2 control-label">密码</label> 16 <div class="col-md-10"> 17 <input asp-for="UserPwd" required="required" class="form-control" /> 18 <span asp-validation-for="UserPwd" class="text-danger"></span> 19 </div> 20 </div> 21 <div class="form-group"> 22 <label asp-for="ComfirmPwd" class="col-md-2 control-label">确认密码</label> 23 <div class="col-md-10"> 24 <input asp-for="ComfirmPwd" class="form-control" /> 25 <span asp-validation-for="ComfirmPwd" class="text-danger"></span> 26 </div> 27 </div> 28 29 <div class="form-group"> 30 <div class="col-md-offset-2 col-md-10"> 31 <input type="button" value="提 交" name="btnSubmit" class="btn btn-default" /> 32 <span id="msgbox" style="color:red">@ViewData["msgbox"]</span> 33 </div> 34 </div> 35 </div> 36 </form> 37 <br /> 38 <div> 39 <a href="/member/login">有账号去登录</a> | <a href="/member/register">没账号这里注册</a> 40 </div> 41 <script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script> 42 <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
好的,此此盘文章很长,如果您有耐心读完相信您能有好的收获,一天一进步,别人的经验只要您学习到了同样也是自己的了,就算您没有做过类似的功能或者系统,当您读完后您也有大概的思路了呢;如果帮助,请不吝点个“推荐”,谢谢。