学编程,来源栈;先学习,再交钱
当前系列: ASP.NET 修改讲义
演示#理解#:
  • 注册:把用户名和密码存到数据库
  • 登录:把当前用户输入的用户名和密码,和数据库里保存的用户名和密码进行比对。如果一致,确认身份允许登录。


captcha

又称为图片验证码。

@想一想@为什么需要?(复习:DDOS攻击

复习:如何在@Html.TextBoxFor()中设置宽度?

图片的生成和输出已经学过了,因为要用到captcha的地方很多,所以我们专门的搞一个SharedController,里面放_GetCaptcha(),所以前台:

但后台的比对呢,把用户的输入和谁对比?

利用Session

首先我们要在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), "* 验证码错误");

JavaScript效果

演示:

  1. F12查看文本框的focus事件(#体会#:前端无秘密)
  2. 如果刷新验证码时,不在src的url后添加url参数,图片被缓存后不会刷新
$('[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可以一并代入。

Filter

新建:

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中重用……


密码MD5加密

复习:明文密码泄露灾难

出于安全考虑,.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,但cookie的内容是什么?

  • 用户Id:以便确定用户的身份
  • 用户(加密后)密码,确保用户cookie没有被伪造

PS:如果觉得用户(加密后)密码直接暴露给前台还是不安全,还可以用系统自动生成的token(略)

cookie什么时候过期呢?可以把这个选择权交给用户

checkbox记住我

@想一想@:有没有必要在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中

  • 如果用户没有登录,显示登录和注册的链接;否则
  • 显示:欢迎你,某某某(用户名)

如何实现?

Layout

因为登录状态是所有页面共享的,所以我们大概要把一段 if...else... 的代码放在Layout中。

Layout中确实能够直接拿到Request对象,

@{ 
    //bool hasLogon = Request.Cookies["..."].Values["..."]
    if (hasLogon) { }
    else
但是拿到cookie对象之后,还有调用repository比对密码的逻辑……这些都放在Layout中不合适!

然后,Layout是没有对象的Controller和Action的,咋整?可以利用

ChildAction

在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);


NeedLogOn

在需要登录用户才能访问的页面(比如:/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是“不稳定的”:(演示)

  1. 用户第一次登录失败
  2. 未登录用户想起他根本没有注册,于是通过登录页面的链接跳转到注册页面,完成注册之后,他应该/实际上会被重定向到哪个页面?

所以为了更好的用户体验,我们应该自行记录“之前页面”(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)}";


封装@Html.Prepage()【难】


作业

  1. 注册成功后自动登录(不用记住我)
  2. 利用cookie完成“返回之前页面”功能
  3. 完成其他全部注册/登录功能
  4. 修改密码,注意:
    1. 登录用户才能修改
    2. 修改之前检查用户输入的原密码是否正确  
  5. (利用位运算赋权限)将用户分为:管理员(admin)、博主(blogger)和注册用户(registered),然后按一起帮完成导航栏

学习笔记
源栈学历
键盘敲烂,月薪过万作业不做,等于没学

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 ASP.NET 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码