Identity是Asp.Net Core全新的一个用户管理系统,它是一个完善的全面的庞大的框架,提供的功能有:
-
创建、查询、更改、删除账户信息
-
验证和授权
-
密码重置
-
双重身份认证
-
支持扩展登录,如微软、Facebook、google、QQ、微信等
-
提供了一个丰富的API,并且这些API还可以进行大量的扩展
接下来我们先来看下它的简单使用。首先在我们的DbContext中需要继承自IdentityDbContext。
public class AppDbContext:IdentityDbContext { public AppDbContext(DbContextOptions<AppDbContext> options):base(options) { } public DbSet<Student> Students { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Seed(); } }
然后在Startup中注入其依赖,IdentityUser和IdentityRole是Identity框架自带的两个类,将其绑定到我们定义的AppDbContext中。
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>();
最后需要添加中间件UseAuthentication。
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { //如果环境是Development,调用 Developer Exception Page if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); app.UseStatusCodePages(); app.UseStatusCodePagesWithReExecute("/Error/{0}"); } app.UseStaticFiles(); app.UseAuthentication(); app.UseMvc(routes => { routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}"); }); }
接下来我们就可以使用数据库迁移 Add-Migration 来添加迁移,然后update-database我们的数据库。
可以在数据库中看到其生成的表。
在完成数据迁移之后,我们再来看下Identity中如何完成用户的注册和登录。
我们定义一个ViewModel,然后定义一个AccountController来完成我们的注册和登录功能。Asp.Net Core Identity为我们提供了UserManger来对用户进行增删改等操作,提供了SignInManager的SignInAsync来登录,SignOutAsync来退出,IsSignedIn来判断用户是否已登录等。
public class RegisterViewModel { [Required] [Display(Name = "邮箱地址")] [EmailAddress] public string Email { get; set; } [Required] [Display(Name = "密码")] [DataType(DataType.Password)] public string Password { get; set; } [DataType(DataType.Password)] [Display(Name = "确认密码")] [Compare("Password", ErrorMessage = "密码与确认密码不一致,请重新输入.")] public string ConfirmPassword { get; set; } } public class LoginViewModel { [Required] [EmailAddress] public string Email { get; set; } [Required] [DataType(DataType.Password)] public string Password { get; set; } [Display(Name = "记住我")] public bool RememberMe { get; set; } }
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using StudentManagement.ViewModels; using System.Threading.Tasks; namespace StudentManagement.Controllers { public class AccountController:Controller { private UserManager<IdentityUser> userManager; private SignInManager<IdentityUser> signInManager; public AccountController(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager) { this.userManager = userManager; this.signInManager = signInManager; } [HttpGet] public IActionResult Register() { return View(); } [HttpPost] public async Task<IActionResult> Register(RegisterViewModel model) { if (ModelState.IsValid) { //将数据从RegisterViewModel复制到IdentityUser var user = new IdentityUser { UserName = model.Email, Email = model.Email }; //将用户数据存储在AspNetUsers数据库表中 var result = await userManager.CreateAsync(user, model.Password); //如果成功创建用户,则使用登录服务登录用户信息 //并重定向到home econtroller的索引操作 if (result.Succeeded) { await signInManager.SignInAsync(user, isPersistent: false); return RedirectToAction("index", "home"); } //如果有任何错误,将它们添加到ModelState对象中 //将由验证摘要标记助手显示到视图中 foreach (var error in result.Errors) { if (error.Code== "PasswordRequiresUpper") { error.Description = "密码必须至少有一个大写字母('A'-'Z')。"; } //PasswordRequiresUpper //Passwords must have at least one uppercase ('A'-'Z'). ModelState.AddModelError(string.Empty, error.Description); } } return View(model); } [HttpPost] public async Task<IActionResult> Logout() { await signInManager.SignOutAsync(); return RedirectToAction("index", "home"); } [HttpGet] [AllowAnonymous] public IActionResult Login() { return View(); } [HttpPost] [AllowAnonymous] public async Task<IActionResult> Login(LoginViewModel model, string returnUrl) { if (ModelState.IsValid) { var result = await signInManager.PasswordSignInAsync( model.Email, model.Password, model.RememberMe, false); if (result.Succeeded) { if (!string.IsNullOrEmpty(returnUrl)) { return Redirect(returnUrl); } else { return RedirectToAction("index", "home"); } } ModelState.AddModelError(string.Empty, "登录失败,请重试"); } return View(model); } } }
@model RegisterViewModel @{ ViewBag.Title = "用户注册"; } <h1>用户注册</h1> <div class="row"> <div class="col-md-12"> <form method="post"> <div asp-validation-summary="All" class="text-danger"></div> <div class="form-group"> <label asp-for="Email"></label> <input asp-for="Email" class="form-control" /> <span asp-validation-for="Email" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Password"></label> <input asp-for="Password" class="form-control" /> <span asp-validation-for="Password" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="ConfirmPassword"></label> <input asp-for="ConfirmPassword" class="form-control" /> <span asp-validation-for="ConfirmPassword" class="text-danger"></span> </div> <button type="submit" class="btn btn-primary">注册</button> </form> </div> </div>
@model LoginViewModel @{ ViewBag.Title = "用户登录"; } <h1>用户登录</h1> <div class="row"> <div class="col-md-12"> <form method="post"> <div asp-validation-summary="All" class="text-danger"></div> <div class="form-group"> <label asp-for="Email"></label> <input asp-for="Email" class="form-control" /> <span asp-validation-for="Email" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Password"></label> <input asp-for="Password" class="form-control" /> <span asp-validation-for="Password" class="text-danger"></span> </div> <div class="form-group"> <div class="checkbox"> <label asp-for="RememberMe"> <input asp-for="RememberMe" /> @Html.DisplayNameFor(m => m.RememberMe) </label> </div> </div> <button type="submit" class="btn btn-primary">登录</button> </form> </div> </div>
在实际工作中,我们需要配置密码的复杂度来增强用户信息的安全性。而Asp.Net Core Identity也默认也提供了一套机制PasswordOptions,可以查看其源码。
https://github.com/aspnet/AspNetCore/blob/master/src/Identity/Extensions.Core/src/PasswordOptions.cs
但是有时候我们需要自定义我们的密码校验模式,这时候可以在Startup中注入
services.Configure<IdentityOptions>(options => { options.Password.RequiredLength = 6; options.Password.RequiredUniqueChars = 3; options.Password.RequireUppercase = false; options.Password.RequireLowercase = false; options.Password.RequireNonAlphanumeric = false; });
同时,我们希望我们在注册的时候提示错误信息时使用中文显示,可以定义一个继承IdentityErrorDescriber的类。
using Microsoft.AspNetCore.Identity; namespace StudentManagement.Middleware { public class CustomIdentityErrorDescriber : IdentityErrorDescriber { public override IdentityError DefaultError() { return new IdentityError { Code = nameof(DefaultError), Description = $"发生了未知的故障。" }; } public override IdentityError ConcurrencyFailure() { return new IdentityError { Code = nameof(ConcurrencyFailure), Description = "乐观并发失败,对象已被修改。" }; } public override IdentityError PasswordMismatch() { return new IdentityError { Code = nameof(PasswordMismatch), Description = "密码错误" }; } public override IdentityError InvalidToken() { return new IdentityError { Code = nameof(InvalidToken), Description = "无效的令牌." }; } public override IdentityError LoginAlreadyAssociated() { return new IdentityError { Code = nameof(LoginAlreadyAssociated), Description = "具有此登录的用户已经存在." }; } public override IdentityError InvalidUserName(string userName) { return new IdentityError { Code = nameof(InvalidUserName), Description = $"用户名'{userName}'无效,只能包含字母或数字." }; } public override IdentityError InvalidEmail(string email) { return new IdentityError { Code = nameof(InvalidEmail), Description = $"Email '{email}' is invalid." }; } public override IdentityError DuplicateUserName(string userName) { return new IdentityError { Code = nameof(DuplicateUserName), Description = $"User Name '{userName}' is already taken." }; } public override IdentityError DuplicateEmail(string email) { return new IdentityError { Code = nameof(DuplicateEmail), Description = $"Email '{email}' is already taken." }; } public override IdentityError InvalidRoleName(string role) { return new IdentityError { Code = nameof(InvalidRoleName), Description = $"Role name '{role}' is invalid." }; } public override IdentityError DuplicateRoleName(string role) { return new IdentityError { Code = nameof(DuplicateRoleName), Description = $"Role name '{role}' is already taken." }; } public override IdentityError UserAlreadyHasPassword() { return new IdentityError { Code = nameof(UserAlreadyHasPassword), Description = "User already has a password set." }; } public override IdentityError UserLockoutNotEnabled() { return new IdentityError { Code = nameof(UserLockoutNotEnabled), Description = "Lockout is not enabled for this user." }; } public override IdentityError UserAlreadyInRole(string role) { return new IdentityError { Code = nameof(UserAlreadyInRole), Description = $"User already in role '{role}'." }; } public override IdentityError UserNotInRole(string role) { return new IdentityError { Code = nameof(UserNotInRole), Description = $"User is not in role '{role}'." }; } public override IdentityError PasswordTooShort(int length) { return new IdentityError { Code = nameof(PasswordTooShort), Description = $"密码必须至少是{length}字符." }; } public override IdentityError PasswordRequiresNonAlphanumeric() { return new IdentityError { Code = nameof(PasswordRequiresNonAlphanumeric), Description = "密码必须至少有一个非字母数字字符." }; } public override IdentityError PasswordRequiresDigit() { return new IdentityError { Code = nameof(PasswordRequiresDigit), Description = $"密码必须至少有一个数字('0'-'9')." }; } public override IdentityError PasswordRequiresUniqueChars(int uniqueChars) { return new IdentityError { Code = nameof(PasswordRequiresUniqueChars), Description = $"密码必须使用至少不同的{uniqueChars}字符。" }; } public override IdentityError PasswordRequiresLower() { return new IdentityError { Code = nameof(PasswordRequiresLower), Description = "密码必须至少有一个小写字母('a'-'z')." }; } public override IdentityError PasswordRequiresUpper() { return new IdentityError { Code = nameof(PasswordRequiresUpper), Description = "密码必须至少有一个大写字母('A'-'Z')." }; } } }
最后需要在注入Identity的时候添加上这个类
services.AddIdentity<IdentityUser, IdentityRole>() .AddErrorDescriber<CustomIdentityErrorDescriber>() .AddEntityFrameworkStores<AppDbContext>();
完成登录后,我们需要对访问资源进行授权,需要在controller或者action上使用Authorize属性来标记,也可以使用AllowAnonymous来允许匿名访问,在项目中使用授权需要引入中间件UseAuthentication
app.UseAuthentication();
但是如果项目中有很多controller需要添加Authorize属性,我们可以在startup中添加全局的授权,代码如下。
services.AddMvc(config => { var policy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); config.Filters.Add(new AuthorizeFilter(policy)); });
一般在用户登录成功后需要重定向到原始的 URL,这个通过请求参数中带returnUrl来实现,但是如果没有判断是否本地的Url时则会引发开放式重定向漏洞。
解决开放式重定向漏洞的方式也很简单,就是在判断的时候添加Url.IsLocalUrl或者直接return LocalRedirect。
if (Url.IsLocalUrl(returnUrl)) { } return LocalRedirect(returnUrl);