又称为图片验证码。
@想一想@:为什么需要?(复习:DDOS攻击)
复习:如何在@Html.TextBoxFor()中设置宽度?
图片的生成和输出已经学过了,因为要用到captcha的地方很多,所以我们专门的搞一个SharedController,里面放_GetCaptcha(),所以前台:
但后台的比对呢,把用户的输入和谁对比?
首先我们要在captcha生成时将其“图片值”存起来,存在哪里?
#理解#:captcha是“用户特定”的,即“一个用户一个captcha”。
但是注意:不要将captcha的图片值放在cookie中——客户端可见不安全。最好的办法是将其存储在session中:
string captcha = "24h7" /*实际上应随机生成*/; Session[Keys.Captcha] = captcha;
然后,在后台比对用户输入和Session中保存的值:
object captcha = Session[Const.Session.CAPTCHA];
if (captcha == null) //@想一想@:为什么会这样? {
ModelState.AddModelError(nameof(student.Captcha), "* 验证码失效");
//...
if (captcha.ToString() != student.Captcha)
{
ModelState.AddModelError(nameof(student.Captcha), "* 验证码错误");
演示:
$('[zyf-captcha-refresh]').click(function () { $(this).attr('src', "/Shared/_GetCaptcha?v="/* + Math.random()*/);
captcha会在很多地方使用(比如/Log/On),所以应该可以被封装重用。
强烈建议:自己先尝试!体会难点。
我们使用EditorFor:(@想一想@:能不能使用Partial()/Action()?)
@Html.EditorFor(m => m.Captcha, "_Captcha")
在Shared/EditorTemplates下添加一个_Captcha.cshtml
@Html.TextBoxFor(m => m, new { style = "width: 60px;" }) <img src="/Shared/_GetCaptcha" zyf-captcha-refresh /> @Html.ValidationMessageFor(m => m) <script>@*略*@</script>
演示:生成的HTML标签中有unobtrusive用属性
#体会#:m=>m也是可以的,^_^
说明:相关JavaScript可以一并代入。新建:
public class CaptchaValidateAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext)通过Request.Form获取用户输入,注意仍然使用的Const字符串:(@想一想@:能不能从Model中取?)
string input = filterContext.HttpContext.Request.Form[Const.Form.CAPTCHA_VALUE];
说明:在ViewData中的Model,不同于Action中作为参数传入的、Model绑定的“model”
也可以使用nameof:
CaptchaModel model = new CaptchaModel(); string input = filterContext.HttpContext.Request.Form[nameof(model.Captcha)];
这里可以抛异常!引导开发人员检查原因:(#体会#:异常的使用)
if (string.IsNullOrWhiteSpace(input))
为了保证captcha的name始终“正确”,可以定义一个基类:
public class CaptchaModel { //[Required等略 public string Captcha { get; set; }
然后让需要captcha验证的Model继承:
public class Student : CaptchaModel
如果验证未通过,ModelState中添加error:
if (input != filterContext.HttpContext.Session[Const.Session.CAPTCHA].ToString()) { filterContext.Controller.ViewData.ModelState.AddModelError( Const.Form.CAPTCHA_VALUE, "* 验证码错误");很明显,这需要和ModelValidationAttribute配合,并只能在ModelValidationAttribute之前执行。可能你想到的是:
[CaptchaValidate(Order = -1)]
但更优雅的方式是在CaptchaValidateAttribute的构造函数中:
public CaptchaValidateAttribute() { Order = Const.FilterOrder.Captcha;
@想一想@:为什么整数Order都要用常量呢?
public class FilterOrder { public const int Captcha = -1; public const int ModelValidate = 0;
提供一个“集中的”地方管理这些Order而不是散得到处都是……
演示:/Log/On中重用……
复习:明文密码泄露灾难
出于安全考虑,.NET并没有为我们提供现成的“字符串到字符串”的MD5加密算法
我们可以首先给明文“加盐”(引入一些混淆字符)
source = source + "17bang"; //所谓的“加盐”,让破解更困难
,然后“自定义的”生成结果字符串
//MD5实现了IDisposable using (MD5 md5Hash = MD5.Create())
//将明文转换成Hash过后的byte[] byte[] data = md5Hasher.ComputeHash(Encoding.Default.GetBytes(source)); //将byte[]转换成16进制的字符 StringBuilder sBuilder = new StringBuilder(); for (int i = 0; i < data.Length/*-1 坑人*/; i++) { sBuilder.Append(data[i].ToString("x2")); } return sBuilder.ToString();
安利使用扩展方法:
public static class StringExtensions { public static string MD5Encrypt(this string source)
【重要】算法一旦确定,绝对不要改变!
为了延迟引入数据库,我们使用模拟的Repository(复习),可以存取User对象。
我们利用Repository,“根据用户名得到用户”:
可能会验证不通过,比如没有找到相应的用户:
Student fromRepo = new StudentRepoitory().GetByName(student.UserName); if (fromRepo == null)
接下来怎么办呢?
ModelState.AddModelError(nameof(student.UserName), "* 用户名不存在"); //不是:return View(student); 而是: TempData[Const.Other.MODEL_VALIDATE_ERROR] = ModelState; return RedirectToAction(nameof(On));
#体会#:在一个项目中,自始至终的整齐划一
通过验证,就应该生成cookie,但cookie的内容是什么?
PS:如果觉得用户(加密后)密码直接暴露给前台还是不安全,还可以用系统自动生成的token(略)
cookie什么时候过期呢?可以把这个选择权交给用户
@想一想@:有没有必要在Student里面声明一个RememberMe的属性?
public bool RememberMe { get; set; }
不建议这样做,因为这里的Student对象属于entity的范畴,它的所有属性应该都是被持久化的(保存到数据库的)。RememberMe明显不属于这个范畴,它明显就用于UI层(详见架构)
所以我们使用:
@Html.CheckBox("RememberMe") 记住我
复习#体会#:Model绑定也适用于简单Action参数
public ActionResult On(Student student, bool rememberMe)
if (rememberMe) { cookie.Expires = DateTime.Now.AddDays(60);
演示并简化逻辑,在Layout中:
如何实现?
因为登录状态是所有页面共享的,所以我们大概要把一段 if...else... 的代码放在Layout中。
Layout中确实能够直接拿到Request对象,
@{ //bool hasLogon = Request.Cookies["..."].Values["..."] if (hasLogon) { } else但是拿到cookie对象之后,还有调用repository比对密码的逻辑……这些都放在Layout中不合适!
然后,Layout是没有对象的Controller和Action的,咋整?可以利用
在Views/Shared下新建一个_LogonStatus.cshtml,根据需求,为了获取用户名和Id,我们还引入了model:
@model _17bang.Models.Student @if (Model == null) { <a href="/Register">注册</a> <span>|</span> <a href="/Log/On">登录</a> } else { <a href="/User/@Model.Id">欢迎你,@Model.UserName</a> }然后在Layout中调用:
@Html.Action("_LogonStatus", "Shared")最后,补齐Action中的逻辑:
HttpCookie cookie = Request.Cookies[_17bang.Const.Cookie.User]; if (cookie == null) { return PartialView();
这样view里面的Model就是null。
int id = Convert.ToInt32(cookie.Values[Const.Cookie.UserId]);@想一想@:这里为什么不用int.TryParse()避免异常被抛出?
通过id获取Student对象,并比对password:
if (student.Password != cookie.Values[Const.Cookie.Password]) { throw new Exception( $" 从cookie中获取的pwd(userid={id})和数据库不符");
说明:抛出的异常信息应尽可能的详细。
最后,一切OK,将Student对象传递给view:
return PartialView(student);
演示:登录后状态发生改变。
#试一试#:登录时如果没有使用PRG模式,会是什么效果?!@想一想@:为什么?
本质就是删除包含用户登录信息的cookie。
怎么实现呢?点击链接:
<a href="/Log/Off">退出登录</a>
在对应的Action中删除储存用户信息的cookie(注意不要Request.Cookie……)
然而,这样会导致跳转到/Log/Off页面(报错找不到Off.cshtml)(演示)
正确的做法是“刷新当前页面”(效果),实际上是“重定向到之前页面”:
return Redirect(Request.UrlReferrer.PathAndQuery);
在需要登录用户才能访问的页面(比如:/Article/New)上添加特性:
[NeedLogOn] public ActionResult New()
在NeedLogOn中,从cookie中获取当前用户信息……但是等等,这样的代码好像之前(LogonStatus)写过?
所以我们可以进行封装重用:
public class CookieHelper { public static Student GetCurrent() { HttpCookie cookie = HttpContext.Current.Request.Cookies[_17bang.Const.Cookie.User];
注意:HttpContext.Current.Request代替了之前的Request。
没有登录的用户访问这些页面,被自动重定向到登录页面:
if (CookieHelper.GetCurrent() == null) { filterContext.Result = new RedirectResult("/Log/On");
当用户完成登录之后,应该可以自动
你可能想到继续使用UrlReferrer,但是UrlReferrer是“不稳定的”:(演示)
所以为了更好的用户体验,我们应该自行记录“之前页面”(prepage)
可以使用cookie/session,或者其他方式,但我们这里给大家演示用URL参数:
string prepage = filterContext.HttpContext.Server.UrlEncode( filterContext.HttpContext.Request.Url.PathAndQuery); filterContext.Result = new RedirectResult( $"/Log/On?prepage={prepage}");加上了prepage参数,值为当前PathAndQuery(注意URLEncode,演示区别)
然后在Log/On的POST Action中:
public ActionResult On(Student student, bool rememberMe, string prepage)
return Redirect(prepage);然后我们想到,应该是任何时候,我们注册/登录之后都应该重定向到之前页面?
所以我们导航栏上注册/登录链接一样可以加prepage参数:
string prepageFormat = $"{UrlParam.PrePage}={Server.UrlEncode(Request.Url.PathAndQuery)}"
<a href="/Register?@prepageFormat">注册</a>
但这样又会带来新问题(如之前场景2,演示)
一个简单的解决方案:如果当前请求的URL中已有prepage参数时,使用原有prepage参数……
string prepageValue = Request.QueryString[UrlParam.PrePage]; string prepageFormat = string.IsNullOrEmpty(prepageValue) ? $"{UrlParam.PrePage}={Server.UrlEncode(Request.Url.PathAndQuery)}" : $"{UrlParam.PrePage}={Server.UrlEncode(prepageValue)}";
暂略
多快好省!前端后端,线上线下,名师精讲
更多了解 加: