推荐关注「码侠江湖」加星标,时刻不忘江湖事
这是 ASP.NET Core Identity 系列的第三篇文章,上一篇文章讲解了如何在 ASP.NET Core Identity 中实现用户注册。
那么,这篇文章讲一讲如何在 ASP.NET Core Identity 中实现用户的登录与登出。
点击上方或后方蓝字,阅读 ASP.NET Core Identity 系列合集。
本篇文章的示例项目:https://github.com/zilor-net/IdentitySample/tree/main/Sample03
身份认证
说到用户登录,就很容易的想到身份认证,这是确认用户身份的过程。
这个过程通过一系列操作,根据数据库中用户留存的凭证,去验证用户提交的凭证,这里的凭证一般就是账号和密码。
为了使用户能够提供凭证,应用程序就需要一个登陆页面,通过提供登录表单,与用户进行交互。
为了实现登陆操作,我们要做的第一件事,就是禁止未经认证的用户,访问 Home
控制器中的 Employees
操作方法。
为此,我们必须为这个操作,添加[Authorize]
特性:
[Authorize]
public async Task<IActionResult> Employees()
然后,在启动类中注册身份认证与授权中间件:
// 认证
app.UseAuthentication();
// 授权
app.UseAuthorization();
需要注意的是,由于管道中间件有执行顺序,所以身份认证中间件,必须在授权中间件之前注册。
如果此时运行应用程序,并单击 Employees
链接,我们会看到一个 404 Not Found 的响应。
之所以会出现这种情况,是因为默认情况下,ASP.NET Core Identity 会尝试将未认证的用户,重定向到 /Account/Login
以引导用户登录,然而这个路由对应的操作我们并没有提供。
另外,我们还可以在地址栏中看到一个 ReturnUrl
的查询参数,它提供了用户重定向到登录页面之前的操作路径。
也就是说,当用户登陆成功后,会重定向回登陆之前的页面。
登录
现在,让我们创建与登录相关的东西。
首先是用户登录模型,用来接受登录表单中用户提交的登录凭证。
在 「Models」 文件夹中,创建一个 「UserLoginModel」 类:
public class UserLoginModel
{[Display(Name = "电子邮箱")][Required(ErrorMessage = "电子邮箱不能为空")][EmailAddress(ErrorMessage = "电子邮箱格式不正确")]public string Email { get; set; }[Display(Name = "密码")][Required(ErrorMessage = "密码不能为空")][DataType(DataType.Password)]public string Password { get; set; }[Display(Name = "记住账号")]public bool RememberMe { get; set; }
}
接下来,在 「AccountController」 控制器中,创建 「Login」 操作方法:
[HttpGet]
public IActionResult Login()
{return View();
}[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(UserLoginModel userModel)
{return View();
}
以及包含登录表单的 「Login」 视图:
现在这个登录视图,只有通过访问受保护的操作,才能够被访问。
但这不符合常理,我们必须提供一个单独的登录链接,让我们修改 _LoginPartial 分部视图:
<ul class="navbar-nav"><li class="nav-item"><a class="nav-link text-dark" asp-controller="Account"asp-action="Login">登录</a></li><li class="nav-item"><a class="nav-link text-dark" asp-controller="Account"asp-action="Register">注册</a></li></ul>
启动应用,可以重复上一次的操作,验证登录页面是否会被打开:
当我们点击登录按钮时,表单数据会被提交到 POST 请求的 「Login」 操作,但是现在还没有任何登录逻辑。
接下来,让我们修改 Login 方法,实现登录逻辑:
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(UserLoginModel userModel)
{if(!ModelState.IsValid){return View(userModel);}var user = await _userManager.FindByEmailAsync(userModel.Email);if(user != null && await _userManager.CheckPasswordAsync(user, userModel.Password)){var identity = new ClaimsIdentity(IdentityConstants.ApplicationScheme);identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id));identity.AddClaim(new Claim(ClaimTypes.Name, user.UserName));// 可以添加更多自定义用户信息identity.AddClaim(new Claim("firstname", user.FirstName));identity.AddClaim(new Claim("lastname", user.LastName));var roles = await _userManager.GetRolesAsync(user);foreach (var role in roles){identity.AddClaim(new Claim(ClaimTypes.Role, role));}await HttpContext.SignInAsync(IdentityConstants.ApplicationScheme,new ClaimsPrincipal(identity));return RedirectToAction(nameof(HomeController.Index), "Home");}else{ModelState.AddModelError("", "无效的用户名或密码");return View();}
}
解释一下这段代码,验证模型是否无效,如果无效,就直接返回视图。
之后,使用 「UserManager」 中的 「FindByEmailAsync」 方法,通过电子邮件查询用户。
使用 「CheckPasswordAsync」 方法,检查用户密码是否与数据库中的哈希密码匹配。
如果用户存在,并且密码验证通过,就创建一个 「ClaimsIdentity」 对象。
ClaimsIdentity
代表身份对象,其中包含两个声明:ID和用户名。
当然,你也可以添加更多的自定义用户信息,比如姓名、角色等。
ApplicationScheme
表示身份方案的名称,这是一个预定义好的静态变量,最终体现为一个名称为 「Identity.Application」 的 Cookie。
之后,通过 「SignInAsync」 方法进行登录,第一个参数就是刚才的方案名称,第二个参数是一个身份持有对象,也就是真正代表用户的对象,我们需要给它提供一个身份。
这个方法会在我们的浏览器中,创建名为 「Identity.Application」 的 Cookie 数据,其值就是身份对象中的信息。
登陆成功后,会将用户重定向到之前的 「Index」 页面。
如果数据库中不存在该用户,或密码不匹配,那就返回一个带有错误消息的视图。
接着,修改一下 「Employees」 视图,让我们登录后可以看到身份信息:
<h2>Claim details</h2>
<ul>@foreach (var claim in User.Claims){<li><strong>@claim.Type</strong>: @claim.Value</li>}
</ul>
现在启动应用,点击 「Employees」 连接,由于我们现在通过认证,所以会跳转到登录页面。
使用刚才注册的用户登录,然后再次点击 「Employees」 连接,可以看到 「Employees」 的数据表格,以及下方的身份信息了。
我们还可以查看浏览器里的 Cookies ,可以看到有两个 Cookie:
「.AspNetCore.Identity.Application」 中保存了身份信息;
「.AspNetCore.Antiforgery.xxxxx」 中保存了验证表单的令牌。
跳转源地址
不过现在还有个小问题,前面我说过,如果用户未经授权访问受保护的操作,就会将被重定向到 Login 页面。
此时,URL 中会包含一个 「ReturnUrl」 查询参数,该参数显示用户来自的源页面。
但在我们的示例中,我们直接将用户导航到了 「/Home/Index」,而不是跳转到 ReturnUrl 中的源页面。
想要实现这个功能,我们需要修改 Get 请求的 「Login」 操作:
[HttpGet]
public IActionResult Login(string returnUrl = null)
{ViewData["ReturnUrl"] = returnUrl;return View();
}
然后,修改 「Login.cshtml」 视图文件:
<form asp-action="Login" asp-route-returnUrl="@ViewData["ReturnUrl"]">
通过 ViewData 将 「returnUrl」 的值,传到视图的表单中的路由参数。
当提交表单时,「ReturnUrl」 就会通过路由参数,提交给 POST 请求的 「Login」 操作。
所以,我们还需要修改 POST 请求的 「Login」 操作:
public async Task<IActionResult> Login(UserLoginModel userModel, string returnUrl = null)
先添加一个 returnUrl 参数,然后创建一个用来重定向的普通方法:
private IActionResult RedirectToLocal(string returnUrl)
{if (Url.IsLocalUrl(returnUrl))return Redirect(returnUrl);elsereturn RedirectToAction(nameof(HomeController.Index), "Home");}
这个方法会先检查 「returnUrl」 是不是本地 URL,如果是,就将用户重定向到该地址,否则,就将用户重定向到主页。
最后,修改 「Login」 操作的返回值,调用刚才添加的方法:
return RedirectToLocal(returnUrl);
启动应用,可以看到,现在已经可以正确跳转到源地址了。
需要说明的是,我们的登录操作位于 「/Account/Login」 路由地址,这是 ASP.NET Core Identity 的默认登录路由地址。
如果你不想使用默认的地址,可以在服务配置方法中进行配置,比如:
builder.Services.ConfigureApplicationCookie(o => o.LoginPath = "/Authentication/Login");
简化登录
前面我们演示的身份认证是完全版的。但是,如果你不需要完全控制身份认证的逻辑,那么有一个更简单的方法:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login([FromServices]SignInManager<User> signInManager, UserLoginModel userModel, string returnUrl = null)
{if(!ModelState.IsValid){return View(userModel);}var result = await signInManager.PasswordSignInAsync(userModel.Email, userModel.Password, userModel.RememberMe, false);if (result.Succeeded){return RedirectToLocal(returnUrl);}ModelState.AddModelError("", "无效的用户名或密码");return View();
}
在登录操作中注入 「SignInManager」 服务,使用 「PasswordSignInAsync」 方法,代替之前的验证逻辑。
这个方法接受四个参数:用户名、密码、持久化标志和登录锁定标志。
关于登录锁定功能后面我们再详说,这里先把它设置为false。
这个方法,完成了我们在前面演示的所有登录逻辑。
此外,它还返回具有四个属性值的结果,其中 「Succeeded」 属性代表是否成功。
登出
使用 ASP.NET Core Identity 实现登录就是如此简单,登出就更简单了。
首先,修改 「_LoginPartial」 登录分部视图:
@using Microsoft.AspNetCore.Identity
@using IdentitySample.Entites
@inject SignInManager<User> _signInManager@{var lastname = User.Claims.SingleOrDefault(claim => claim.Type == "lastname")?.Value;
}<ul class="navbar-nav">@if (_signInManager.IsSignedIn(User)){<li class="nav-item"><a class="nav-link text-dark" asp-controller="Home" asp-action="Index" title="Welcome">欢迎 @lastname!</a></li><li class="nav-item"><a class="nav-link text-dark" asp-controller="Account"asp-action="Logout">登出</a></li>}else{<li class="nav-item"><a class="nav-link text-dark" asp-controller="Account"asp-action="Login">登录</a></li><li class="nav-item"><a class="nav-link text-dark" asp-controller="Account"asp-action="Register">注册</a></li>}
</ul>
在视图中注入 「SignInManager」 服务,使用它来判断用户是否登录,然后渲染不同的片段。
已登录就显示欢迎语与登出按钮,未登录和之前一样。
接下来,实现 「Logout」 登出操作:
public async Task<IActionResult> Logout([FromServices]SignInManager<User> signInManager)
{await signInManager.SignOutAsync();return RedirectToAction(nameof(HomeController.Index), "Home");
}
「SignOutAsync」 方法会通过删除 Cookies,来实现用户的登出。
小结
现在,我们已经实现了用户的登录与登出,具体的代码可以参看示例项目,下篇文章将会讲解用户在忘记密码后,如何通过邮件服务重置密码。
更多精彩内容,请关注我▼▼
如果喜欢我的文章,那么
在看和转发是对我最大的支持!
(戳下面蓝字阅读)
ASP.NET 6 中间件系列
ASP.NET 6 身份认证框架 Identity 系列
查缺补漏系统学习 EF Core 6 系列
推荐关注微信公众号:码侠江湖
觉得不错,点个在看再走哟