身份认证基本每个应用都会需要,从.net版本的form authentication大概就是利用HttpModule填充IPrinciple一个这样的过程,说起来也不算太懂。。
最近在看.net core的身份认证,结合一些网上的资源做一个总结。
背景
identity在.net 4.5时代就有了,.net core应该是很大程度上的重写了(毕竟两个完全不同的环境了)。微软自己的官方文档有一些工具性的介绍,但是太过于依赖vs,通过一些点选,可以容易的搭建出来简单的登录功能,但是自己想修改里面的一部分的时候恐怕就犯难了。
所以在网上找了从头搭建Identity的教程,跟着走了一遍,比利用vs工具稍微熟悉一些。
介绍
Identity基本功能包括用户管理(结合EF或者其他数据库),用户注册、校验等。
涉及到的关键数据结构主要是这两个:
- IdentityUser:Identity自定义的一个特殊用户基类,利用Identity管理的用户都存储在这个结构中,并通过ORM持久化到数据库里
- IdentityDbContext:继承自DbContext,Identity的数据库Context,添加了Identity需要的功能,管理Identity需要使用的各个表。实际使用上一般不会直接操纵它---它其实也不像常用的DbContext那样暴露出一些表的DbSet,实际使用时,往往是操纵Identity的下面两个结构:
Identity提供的作为用户管理的类:
- UserManager:管理用户,注册、查找、找回密码等等,操作用户相关的信息。
- SignInManager:提供用户登录、注销功能。
使用
记录一下Identity基本的使用方法
注册和初始化
分两部分
- 注册服务:
//配置密码强度,这里可以进行其他的配置
services.Configure<IdentityOptions>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequiredLength = 6;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
});
//配置数据库,Identity依赖这个数据库去持久化用户相关数据
services.AddDbContext<FaIdentityDbContext>(options =>
options.UseSqlServer(_config.GetConnectionString("DefaultConnection"))
);
//添加Identity的服务
services.AddIdentity<FaUser, IdentityRole>()
.AddEntityFrameworkStores<FaIdentityDbContext>()
.AddDefaultTokenProviders();
基本的配置记录在注释里,代码中的两个数据结构:
- FaIdentityDbContext是继承自IdentityDbContext,是我自定义的数据库context类
- FaUser是我定义的用户结构,继承自IdentityUser
这两个结构的继承关系是必须的。
使用
用户注册
[HttpPost] public async Task<IActionResult> Register(string email, string password, string repassword) { if (password != repassword) { ModelState.AddModelError(string.Empty, "Password don't match"); return View(); }
var newUser = new FaUser { UserName = email, Email = email }; var userCreationResult = await _userManager.CreateAsync(newUser, password); if (!userCreationResult.Succeeded) { foreach (var error in userCreationResult.Errors) ModelState.AddModelError(string.Empty, error.Description); return View(); } var emailConfirmationToken = await _userManager.GenerateEmailConfirmationTokenAsync(newUser); var tokenVerificationUrl = Url.Action("VerifyEmail", "Account", new { id = newUser.Id, token = emailConfirmationToken }, Request.Scheme); await _messageService.Send(email, "Verify your email", $"Click <a href="{tokenVerificationUrl}">here</a> to verify your email"); return Content("Check your email for a verification link");
}
用户注册依靠UserManager类(通过DI获得),注册后,还可以要求用户通过邮箱验证。
这里通过UserManager生成了邮箱验证的token,附带在用户的验证连接中。
_messageService是发送邮件的一个抽象类,和Identity无关,所以这里略过。
public async Task<IActionResult> VerifyEmail(string id, string token) { var user = await _userManager.FindByIdAsync(id); if (user == null) throw new InvalidOperationException();
var emailConfirmationResult = await _userManager.ConfirmEmailAsync(user, token); if (!emailConfirmationResult.Succeeded) return Content(emailConfirmationResult.Errors.Select(error => error.Description).Aggregate((allErrors, error) => allErrors += ", " + error)); return Content("Email confirmed, you can now log in");
}
通过UserNamager验证邮箱,验证成功时,IdentityUser类中的EmailConfirmed属性会被更新(ConfigEmailAsync方法内部实现的)。
至此用户注册成功
重置密码
[HttpPost] public async Task<IActionResult> ForgotPassword(string email) { var user = await _userManager.FindByEmailAsync(email); if (user == null) return Content("Check your email for a password reset link");
var passwordResetToken = await _userManager.GeneratePasswordResetTokenAsync(user); var passwordResetUrl = Url.Action("ResetPassword", "Account", new { email = user.Email, id = user.Id, token = passwordResetToken }, Request.Scheme); await _messageService.Send(email, "Password reset", $"Click <a href="" + passwordResetUrl + "">here</a> to reset your password"); return Content("Check your email for a password reset link");
}
通过用户邮箱,生成密码重置的token,并制作成链接发送到用户邮箱中。
[HttpPost] public async Task<IActionResult> ResetPassword(string id, string token, string password, string repassword) { var user = await _userManager.FindByIdAsync(id); if (user == null) throw new InvalidOperationException();
if (password != repassword) { ModelState.AddModelError(string.Empty, "Passwords do not match"); return View(); } var resetPasswordResult = await _userManager.ResetPasswordAsync(user, token, password); if (!resetPasswordResult.Succeeded) { foreach (var error in resetPasswordResult.Errors) ModelState.AddModelError(string.Empty, error.Description); return View(); } return Content("Password updated");
}
根据Token,使用UserManager的ResetPasswordAsync方法重置密码
登录和登出
登录和登出通过的SignInManager来操作
所谓的登录(SignIn),实际可理解为将用户信息经过处理(加密)后,写入cookie中,这样后续所有的请求都会携带这些信息。
与之相辅的认证(Authenticate),就是从Cookie中获取登录时写入的用户信息,经过解密分析重新解析出用户信息到ClaimsPrinciple中,存入到HttpContext里供后续授权判断。
登录:
[HttpPost] public async Task<IActionResult> Login(string email, string password, bool rememberMe) { var user = await _userManager.FindByEmailAsync(email); if (user == null) { ModelState.AddModelError(string.Empty, "Invalid login"); return View(); } if (!user.EmailConfirmed) { ModelState.AddModelError(string.Empty, "Confirm your email first"); return View(); }
var passwordSignInResult = await _signInManager.PasswordSignInAsync(user, password, isPersistent: rememberMe, lockoutOnFailure: false); if (!passwordSignInResult.Succeeded) { ModelState.AddModelError(string.Empty, "Invalid login"); return View(); } return Redirect("~/");
}
登出:
[HttpPost]
public async Task<IActionResult> Logout()
{
await _signInManager.SignOutAsync();
return Redirect("~/");
}
值得注意的是,这里登出用的是POST操作,因为登出涉及到状态的改变。