Asp.Net Core 3.x MVC中基于Claim的登录验证实现过程
一、首先导入Nuget包
Microsoft.AspNetcore.Authentication.Cookies
二、修改Startup.cs
- 注册Authentication服务。 在 ConfigureServices 方法中加入以下代码。(直接复制下面的代码粘贴。注意修改登录页面的路径 比如我的是 AccountController里的Login方法 我就写 /Account/Login)
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.LoginPath = new PathString("/Account/Login"); // 登录页面的url
options.AccessDeniedPath = new PathString("/Account/Login");//没有授权跳转的页面
options.ExpireTimeSpan = TimeSpan.FromHours(0.5); // cookies的过期时间
});
2.添加中间件。在 Configure 方法中加入 app.UseAuthentication();
三 、准备工作做好后,写一些简单的代码来实现演示一下流程(代码简单 但是我想尽可能的把过程讲的通俗易懂些 所以可能篇幅冗长)
1.用到的类
LogOnModel:登录时用的类对象 用户名和密码即可
UserLogOnStatus:登录状态
(可能下面的代码中会出现Account类 因为我这里把模型又分为视图模型和实体类所以不必惊慌 ,Account 就是一个用户类有 Id,UserName,Pasword,Email等字段 自己写一下应该没问题)
public class LogOnModel
{
[Required]
public string UserName { get; set; }
[Required]
[Display(Name = "Password")]
public string Password { get; set; }
}
public enum UserLogOnStatus
{
Failed,
User,
DontExist
}
2.登录页面的表单
只需要用户名和密码即可
<form method="post" class="layui-form">
<div class="layui-form-item">
<input name="UserName" placeholder="用户名" type="text" lay-verify="required" class="layui-input" value="@Model.UserName">
</div>
<p> @Html.ValidationMessageFor(m => m.UserName, "", new { @class = "text-danger" })</p>
<div class="layui-form-item">
<input name="Password" lay-verify="required" placeholder="密码" type="password" class="layui-input">
</div>
<p> @Html.ValidationMessageFor(m => m.Password, "", new { @class = "text-danger" })</p>
<div class="layui-form-item">
<input class="loginin" value="登录" lay-submit lay-filter="login" style="width:100%;" type="submit">
</div>
</form>
3.控制器
如果直接将验证写在控制器里的话 必须为异步 代码如下:
过程: 登录页面将用户名信息 (==我这里用的是模型绑定 也可以直接写 string userName,string passWord ==) 提交过来后 对用户名和密码进行验证 (这步自己实现)如果判别用户名密码正确的话 生成 Claim ;根据 Claim 生成 ClaimsIdentity 在根据 ClaimsIdentity 生成 ClaimsPrincipal 然后SingInAsync
扩展:解释一下这些东西是什么 (可能语言组织不好 可以百度进行更深入的理解)
Claim:是对被验证主体的一些特征信息 比如我这里ClaimTypes.Sid代表当前登录用户的Id和 ClaimTypes.Name代表当前用户名;比如身份证上的 姓名 出生年月等的键值形式的信息。
ClaimsIdentity: 一些claims可以构成了一个identity,具有这些claims的identity就是生成的ClaimsIdentity。ClaimsIdentity可以看作一张完整的身份证了。 可以理解为一张令牌通行证之类的。。
ClaimsPrincipal :可以理解为令牌的持有者 按我上面举的身份证的例子的话 ClaimsPrincipal代表你自己。
代码:
public async Task<IActionResult> Login(LogOnModel model)
{
if (ModelState.IsValid)
{
UserSession userSession;
Account user;
var logOnStatus = _accountService.VerifyUserLogon(model, out userSession,out user);
if (logOnStatus == UserLogOnStatus.User)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Sid,user.Id.ToString()),
new Claim(ClaimTypes.Name,user.UserName)
};
var identity = new ClaimsIdentity(claims,loginClaim);
var userPrincipal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, userPrincipal, new AuthenticationProperties
{
ExpiresUtc = DateTime.UtcNow.AddMinutes(20),
IsPersistent = false,
AllowRefresh = false
});
HttpContext.Session.SetString("CurrentUser", userSession.ToString());
return Redirect("/Home/Index");
}
if (logOnStatus == UserLogOnStatus.Failed)
{
ModelState.AddModelError("Password", "密码错误!");
}
else if (logOnStatus == UserLogOnStatus.DontExist)
{
ModelState.AddModelError("UserName", "账户不存在!");
}
}
扩展 。==可能到这里有的人说 这样直接写在控制器里是不是有点生硬,能不能把他封装出来? 可以 但是有点细节 下面仔细看 ==
我这里新建了一个类 代码如下
细节1 这里有个关键点是这样 我们封装出来的话 直接写HttpContext.SignInAsync是不行的,原因是你这里写HttpContext并不是当前请求的HttpContext 。所以现在有个问题就要解决,就是怎样得到当前的 HttpContext。
所以这里要用到 IHttpContextAccessor 至于这是个什么 大家可以去百度了解 这里我只是告诉大家可以用它获取当前 HttpContext .
我这里的做法是在Startup.cs里通过把IHttpContextAccessor注入(我这里因为用的是Autofac 所以写在了 ConfigureContainer )
builder.RegisterType(typeof(HttpContextAccessor)).As(typeof(IHttpContextAccessor)).SingleInstance();
如果没用Autofac的话可以直接写在 ConfigureServices里
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Security.Claims;
namespace PersonalWeb.UI
{
public interface IAuthenticationService
{
public void SignIn(string userId, string userName);
void SignOut();
}
public class AuthenticationService : IAuthenticationService
{
private readonly IHttpContextAccessor _httpContextAccessor;
public AuthenticationService(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public async void SignIn(string userId, string userName)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Sid,userId),
new Claim(ClaimTypes.Name,userName)
};
var identity = new ClaimsIdentity(claims, "loginClaim");
var userPrincipal = new ClaimsPrincipal(identity);
await _httpContextAccessor.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, userPrincipal, new AuthenticationProperties
{
ExpiresUtc = DateTime.UtcNow.AddMinutes(20),
IsPersistent = false,
AllowRefresh = false
});
}
public async void SignOut()
{
await _httpContextAccessor.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
}
细节2 所以现在封装好了之后怎样在控制器中使用呢 两种办法
第一种就是 注释掉的那样 在控制器初始化的时候 判断_authenticationService为空 就给他生成一个
第二种 就是在控制器的构造函数里直接注入(注入方式和上面注入 IHttpContextAccessor 的方式一样)
代码:
private IAuthenticationService _authenticationService { get; set; }
private readonly IAccountService _accountService;
//protected override void Initialize(RequestContext requestContext)
//{
// if (AuthenticationService == null) { AuthenticationService = new AuthenticationService(); }
// base.Initialize(requestContext);
//}
public AccountController(IAccountService accountService, IAuthenticationService authenticationService)
{
_accountService = accountService;
_authenticationService = authenticationService;
}
在控制器中直接调用就可以了
_authenticationService.SignIn(user.Id.ToString(), user.UserName);
4.使用方法
在需要验证的控制器或方法上面加 [Authorize] 就可以了 这样如果在浏览器地址栏直接访问HomeController的话会先进行验证 如果验证不通过就会自动跳到Login页面
不需要验证的地方 加 [AllowAnonymous]
------------------------------------------- 【结束】 --------------------------------------
终于完了。。。 篇幅有点长 见谅