• ASP.NET 身份认证框架 Identity - 登录与登出


    上一篇文章讲解了如何在 ASP.NET Core Identity 中实现用户注册。

    那么,这篇文章讲一讲如何在 ASP.NET Core Identity 中实现用户的登录与登出。

    身份认证

    说到用户登录,就很容易的想到身份认证,这是确认用户身份的过程。

    这个过程通过一系列操作,根据数据库中用户留存的凭证,去验证用户提交的凭证,这里的凭证一般就是账号和密码。

    为了使用户能够提供凭证,应用程序就需要一个登陆页面,通过提供登录表单,与用户进行交互。

    为了实现登陆操作,我们要做的第一件事,就是禁止未经认证的用户,访问 Home 控制器中的 Employees 操作方法。

    为此,我们必须为这个操作,添加[Authorize]特性:

    1. [Authorize]
    2. public async Task Employees()

    然后,在启动类中注册身份认证与授权中间件:

    1. // 认证
    2. app.UseAuthentication();
    3. // 授权
    4. app.UseAuthorization();

    需要注意的是,由于管道中间件有执行顺序,所以身份认证中间件,必须在授权中间件之前注册。

    如果此时运行应用程序,并单击 Employees 链接,我们会看到一个 404 Not Found 的响应。

    cd5aa8ef1670b16ef362b025108522eb.png

    之所以会出现这种情况,是因为默认情况下,ASP.NET Core Identity 会尝试将未认证的用户,重定向到 /Account/Login 以引导用户登录,然而这个路由对应的操作我们并没有提供。

    另外,我们还可以在地址栏中看到一个 ReturnUrl 的查询参数,它提供了用户重定向到登录页面之前的操作路径。

    也就是说,当用户登陆成功后,会重定向回登陆之前的页面。

    登录

    现在,让我们创建与登录相关的东西。

    首先是用户登录模型,用来接受登录表单中用户提交的登录凭证。

    在 「Models」 文件夹中,创建一个 「UserLoginModel」 类:

    1. public class UserLoginModel
    2. {
    3.    [Display(Name = "电子邮箱")]
    4.    [Required(ErrorMessage = "电子邮箱不能为空")]
    5.    [EmailAddress(ErrorMessage = "电子邮箱格式不正确")]
    6.    public string Email { getset; }
    7.    [Display(Name = "密码")]
    8.    [Required(ErrorMessage = "密码不能为空")]
    9.    [DataType(DataType.Password)]
    10.    public string Password { getset; }
    11.    [Display(Name = "记住账号")]
    12.    public bool RememberMe { getset; }
    13. }

    接下来,在 「AccountController」 控制器中,创建 「Login」 操作方法:

    1. [HttpGet]
    2. public IActionResult Login()
    3. {
    4.     return View();
    5. }
    6. [HttpPost]
    7. [ValidateAntiForgeryToken]
    8. public async Task Login(UserLoginModel userModel)
    9. {
    10.     return View();
    11. }

    以及包含登录表单的 「Login」 视图:

    746afaab4b95bb25833c981ffc159342.png

    现在这个登录视图,只有通过访问受保护的操作,才能够被访问。

    但这不符合常理,我们必须提供一个单独的登录链接,让我们修改 _LoginPartial 分部视图:

    1. <ul class="navbar-nav">
    2.     <li class="nav-item">
    3.         <a class="nav-link text-dark" asp-controller="Account"
    4.            asp-action="Login">登录a>
    5.     li>
    6.     <li class="nav-item">
    7.         <a class="nav-link text-dark" asp-controller="Account"
    8.            asp-action="Register">注册a>
    9.     li>
    10. ul>

    启动应用,可以重复上一次的操作,验证登录页面是否会被打开:

    d1aa6d96225ec8a5c3adbe686322ae17.png

    当我们点击登录按钮时,表单数据会被提交到 POST 请求的 「Login」 操作,但是现在还没有任何登录逻辑。

    接下来,让我们修改 Login 方法,实现登录逻辑:

    1. using System.Security.Claims;
    2. using Microsoft.AspNetCore.Authentication;
    3. [HttpPost]
    4. [ValidateAntiForgeryToken]
    5. public async Task Login(UserLoginModel userModel)
    6. {
    7.     if(!ModelState.IsValid)
    8.     {
    9.         return View(userModel);
    10.     }
    11.     var user = await _userManager.FindByEmailAsync(userModel.Email);
    12.     if(user != null && 
    13.        await _userManager.CheckPasswordAsync(user, userModel.Password))
    14.     {
    15.         var identity = new ClaimsIdentity(IdentityConstants.ApplicationScheme);
    16.         identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id));
    17.         identity.AddClaim(new Claim(ClaimTypes.Name, user.UserName));
    18.         // 可以添加更多自定义用户信息
    19.         identity.AddClaim(new Claim("firstname", user.FirstName));
    20.         identity.AddClaim(new Claim("lastname", user.LastName));
    21.         var roles = await _userManager.GetRolesAsync(user);
    22.         foreach (var role in roles)
    23.         {
    24.             identity.AddClaim(new Claim(ClaimTypes.Role, role));
    25.         }
    26.         await HttpContext.SignInAsync(IdentityConstants.ApplicationScheme,
    27.                                       new ClaimsPrincipal(identity));
    28.         return RedirectToAction(nameof(HomeController.Index), "Home");
    29.     }
    30.     else
    31.     {
    32.         ModelState.AddModelError("""无效的用户名或密码");
    33.         return View();
    34.     }
    35. }

    解释一下这段代码,验证模型是否无效,如果无效,就直接返回视图。

    之后,使用 「UserManager」 中的 「FindByEmailAsync」 方法,通过电子邮件查询用户。

    使用 「CheckPasswordAsync」 方法,检查用户密码是否与数据库中的哈希密码匹配。

    如果用户存在,并且密码验证通过,就创建一个 「ClaimsIdentity」 对象。

    ClaimsIdentity 代表身份对象,其中包含两个声明:ID和用户名。

    当然,你也可以添加更多的自定义用户信息,比如姓名、角色等。

    ApplicationScheme 表示身份方案的名称,这是一个预定义好的静态变量,最终体现为一个名称为 「Identity.Application」 的 Cookie。

    之后,通过 「SignInAsync」 方法进行登录,第一个参数就是刚才的方案名称,第二个参数是一个身份持有对象,也就是真正代表用户的对象,我们需要给它提供一个身份。

    这个方法会在我们的浏览器中,创建名为 「Identity.Application」 的 Cookie 数据,其值就是身份对象中的信息。

    登陆成功后,会将用户重定向到之前的 「Index」 页面。

    如果数据库中不存在该用户,或密码不匹配,那就返回一个带有错误消息的视图。

    接着,修改一下 「Employees」 视图,让我们登录后可以看到身份信息:

    1. <h2>Claim detailsh2>
    2. <ul>
    3.     @foreach (var claim in User.Claims)
    4.     {
    5.         <li><strong>@claim.Typestrong>: @claim.Valueli>
    6.     }
    7. ul>

    现在启动应用,点击 「Employees」 连接,由于我们现在通过认证,所以会跳转到登录页面。

    使用刚才注册的用户登录,然后再次点击 「Employees」 连接,可以看到 「Employees」 的数据表格,以及下方的身份信息了。

    f05e69f5aa4d05ef87b12f7cac154a75.png

    我们还可以查看浏览器里的 Cookies ,可以看到有两个 Cookie:

    f4e88dcf29ab9236e59cce54165889e1.png

    「.AspNetCore.Identity.Application」 中保存了身份信息;

    「.AspNetCore.Antiforgery.xxxxx」 中保存了验证表单的令牌。

    跳转源地址

    不过现在还有个小问题,前面我说过,如果用户未经授权访问受保护的操作,就会将被重定向到 Login 页面。

    此时,URL 中会包含一个 「ReturnUrl」 查询参数,该参数显示用户来自的源页面。

    但在我们的示例中,我们直接将用户导航到了 「/Home/Index」,而不是跳转到 ReturnUrl 中的源页面。

    想要实现这个功能,我们需要修改 Get 请求的 「Login」 操作:

    1. [HttpGet]
    2. public IActionResult Login(string returnUrl = null)
    3. {
    4.     ViewData["ReturnUrl"] = returnUrl;
    5.     return View();
    6. }

    然后,修改 「Login.cshtml」 视图文件:

    <form asp-action="Login" asp-route-returnUrl="@ViewData["ReturnUrl"]">

    通过 ViewData 将 「returnUrl」 的值,传到视图的表单中的路由参数。

    当提交表单时,「ReturnUrl」 就会通过路由参数,提交给 POST 请求的 「Login」 操作。

    所以,我们还需要修改 POST 请求的 「Login」 操作:

    public async Task Login(UserLoginModel userModel, string returnUrl = null)

    先添加一个 returnUrl 参数,然后创建一个用来重定向的普通方法:

    1. private IActionResult RedirectToLocal(string returnUrl)
    2. {
    3.     if (Url.IsLocalUrl(returnUrl))
    4.         return Redirect(returnUrl);
    5.     else
    6.         return RedirectToAction(nameof(HomeController.Index), "Home");
    7. }

    这个方法会先检查 「returnUrl」 是不是本地 URL,如果是,就将用户重定向到该地址,否则,就将用户重定向到主页。

    最后,修改 「Login」 操作的返回值,调用刚才添加的方法:

    return RedirectToLocal(returnUrl);

    启动应用,可以看到,现在已经可以正确跳转到源地址了。

    需要说明的是,我们的登录操作位于 「/Account/Login」 路由地址,这是 ASP.NET Core Identity 的默认登录路由地址。

    如果你不想使用默认的地址,可以在服务配置方法中进行配置,比如:

    1. builder.Services.ConfigureApplicationCookie(o =>
    2.  o.LoginPath = "/Authentication/Login");

    简化登录

    前面我们演示的身份认证是完全版的。但是,如果你不需要完全控制身份认证的逻辑,那么有一个更简单的方法:

    1. [HttpPost]
    2. [ValidateAntiForgeryToken]
    3. public async Task Login([FromServices]SignInManager signInManager, UserLoginModel userModel, string returnUrl = null)
    4. {
    5.     if(!ModelState.IsValid)
    6.     {
    7.         return View(userModel);
    8.     }
    9.     var result = await signInManager.PasswordSignInAsync(
    10.         userModel.Email, userModel.Password, 
    11.         userModel.RememberMe, false);
    12.     
    13.     if (result.Succeeded)
    14.     {
    15.         return RedirectToLocal(returnUrl);
    16.     }
    17.     ModelState.AddModelError("""无效的用户名或密码");
    18.     return View();
    19. }

    在登录操作中注入 「SignInManager」 服务,使用 「PasswordSignInAsync」 方法,代替之前的验证逻辑。

    这个方法接受四个参数:用户名、密码、持久化标志和登录锁定标志。

    关于登录锁定功能后面我们再详说,这里先把它设置为false。

    这个方法,完成了我们在前面演示的所有登录逻辑。

    此外,它还返回具有四个属性值的结果,其中 「Succeeded」 属性代表是否成功。

    登出

    使用 ASP.NET Core Identity 实现登录就是如此简单,登出就更简单了。

    首先,修改 「_LoginPartial」 登录分部视图:​​​​​​​

    1. @using Microsoft.AspNetCore.Identity
    2. @using IdentitySample.Entites
    3. @inject SignInManager<User> _signInManager
    4. @{
    5.     var lastname = User.Claims.SingleOrDefault(claim => claim.Type == "lastname")?.Value;
    6. }
    7. <ul class="navbar-nav">
    8.     @if (_signInManager.IsSignedIn(User))
    9.     {
    10.         <li class="nav-item">
    11.             <a class="nav-link text-dark" asp-controller="Home" asp-action="Index" 
    12.                title="Welcome">欢迎 @lastname!
    13.             a>
    14.         li>
    15.         <li class="nav-item">
    16.             <a class="nav-link text-dark" asp-controller="Account"
    17.                asp-action="Logout">登出a>
    18.         li>
    19.     }
    20.     else
    21.     {
    22.         <li class="nav-item">
    23.             <a class="nav-link text-dark" asp-controller="Account"
    24.                asp-action="Login">登录a>
    25.         li>
    26.         <li class="nav-item">
    27.             <a class="nav-link text-dark" asp-controller="Account"
    28.                asp-action="Register">注册a>
    29.         li>
    30.     }
    31. ul>

    在视图中注入 「SignInManager」 服务,使用它来判断用户是否登录,然后渲染不同的片段。

    已登录就显示欢迎语与登出按钮,未登录和之前一样。

    接下来,实现 「Logout」 登出操作:

    1. public async Task Logout([FromServices]SignInManager signInManager)
    2. {
    3.     await signInManager.SignOutAsync();
    4.     return RedirectToAction(nameof(HomeController.Index), "Home");
    5. }

    「SignOutAsync」 方法会通过删除 Cookies,来实现用户的登出。

    小结

    现在,我们已经实现了用户的登录与登出,具体的代码可以参看示例项目,下篇文章将会讲解用户在忘记密码后,如何通过邮件服务重置密码。

  • 相关阅读:
    汉诺塔-python递归
    视频特效-使用ffmpeg滤镜
    企业想上MES系统?还得满足这些条件
    React Native读取系统属性
    大数据平台三大优势详解-行云管家
    JSR303和拦截器
    软考高级系统架构设计师系列论文真题六:论信息系统的安全性与保密性设计
    1.3 Linux目录操作
    Backblaze 2023 Q3硬盘故障质量报告解读
    基于3D分层卷积融合的多模态生理信号情绪识别
  • 原文地址:https://blog.csdn.net/qq_41872328/article/details/126248681