- Tính năng Lockout trong Identity ASP.NET Core
- Kích hoạt Lockout trong tranng Login
- Tính năng lấy lại password (reset password)
Bài này tiếp tục thực hành, phát triển trên dự án của ví dụ Album trước: Identity (1)
Tính năng Lockout trong Identity ASP.NET Core
Tính năng khóa tài khoản tạm thời Lockout của Identity nhằm đăng độ an toàn, tránh tấn công Brute Force (thử password nhiều lần). Tính năng này là khi một tài khoản đăng nhập một số lần liên tục nhất định thất bại thì sẽ bị khóa đăng nhập trong một khoảng giời gian.
Muốn tính năng này hoạt động cần 2 điều kiện:
Phải sử dụng lockout trong phương thức PasswordSignInAsync
Khi đăng nhập tài khoản bằng cách sử dụng password sử dụng phương thức PasswordSignInAsync của SignInManager, phương thức này có dạng
PasswordSignInAsync (string userName, string password, bool isPersistent, bool lockoutOnFailure);
Trong đó - nếu tham số lockoutOnFailure bằng true thì sẽ cập nhật số lần đăng nhập thất bài liên tiếp
vào trường AccessFailedCount
của bảng User trong CSDL.
Điều kiện thứ 2 là phải bật tính năng lockout trong cấu hình Identity
Trong Startup.cs
cấu hình như sau:
public void ConfigureServices (IServiceCollection services) { // ... các thiết lập dịch vụ khác // Truy cập IdentityOptions services.Configure<IdentityOptions> (options => { // ... // Cấu hình Lockout - khóa user options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes (2); // Khóa 2 phút options.Lockout.MaxFailedAccessAttempts = 3; // Thất bại 3 lần thì khóa // ... }); // ... các code khác }
Tính năng Lockout trong trong Log In
Mở Login.cshtml.cs
ra và cập nhật OnPostAsync
public async Task<IActionResult> OnPostAsync (string returnUrl = null) { returnUrl = returnUrl ?? Url.Content ("~/"); // Đã đăng nhập nên chuyển hướng về Index if (_signInManager.IsSignedIn (User)) return Redirect ("Index"); if (ModelState.IsValid) { IdentityUser user = await _userManager.FindByEmailAsync (Input.UserNameOrEmail); if (user == null) user = await _userManager.FindByNameAsync(Input.UserNameOrEmail); if (user == null) { ModelState.AddModelError (string.Empty, "Tài khoản không tồn tại."); return Page (); } var result = await _signInManager.PasswordSignInAsync ( user.UserName, Input.Password, Input.RememberMe, // Có lưu cookie - khi đóng trình duyệt vẫn nhớ true // CÓ ÁP DỤNG LOCKOUT ); if (result.Succeeded) { _logger.LogInformation ("User đã đăng nhập"); return ViewComponent(MessagePage.COMPONENTNAME, new MessagePage.Message() { title = "Đã đăng nhập", htmlcontent = "Đăng nhập thành công", urlredirect = returnUrl }); } if (result.RequiresTwoFactor) { // Nếu cấu hình đăng nhập hai yếu tố thì chuyển hướng đến LoginWith2fa return RedirectToPage ("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe }); } if (result.IsLockedOut) { _logger.LogWarning ("Tài khoản bí tạm khóa."); // Chuyển hướng đến trang Lockout - hiện thị thông báo return RedirectToPage ("./Lockout"); } else { ModelState.AddModelError (string.Empty, "Không đăng nhập được."); return Page (); } } return Page (); }
Khi đăng nhập bằng PasswordSignInAsync trả về SignInResult
mà đạt đến số lần thất bại bị khóa, thì kết quả trả về có SignInResult.Succeed
là false
và
SignInResult.IsLockedOut
là true
Khi kiểm tra IsLockedOut là true ở code trên cho biết sẽ chuyển hướng
if (result.IsLockedOut) { _logger.LogWarning ("Tài khoản bí tạm khóa."); // Chuyển hướng đến trang Lockout - hiện thị thông báo return RedirectToPage ("./Lockout"); }
Trang chuyển hướng đến khi bị khóa là Lockout, nó hiện thị thông báo bị khóa. Bạn có thể mở ra sửa nội dung thành
Lockout.cshtml
@page @model LockoutModel @{ ViewData["Title"] = "Tạm khóa tài khoản"; } <header> <h1 class="text-danger">@ViewData["Title"]</h1> <p class="text-danger">Tài khoản này tạm khóa, vui lòng thử lại sau.</p> </header>
Khi tài khoản đăng nhập mà bị khóa, nó sẽ chuyển đến
Reset Password - Lấy lại password
Khi người dùng quyên mật khẩu, có thể xây dựng tính năng cho người dùng nhập vào email - sau đó ứng dụng gửi email kèm mã token - cho phép đặt lại mật khẩu
ForgotPassword.cshtml.cs
namespace Album.Areas.Identity.Pages.Account { [AllowAnonymous] public class ForgotPasswordModel : PageModel { private readonly UserManager<AppUser> _userManager; private readonly IEmailSender _emailSender; public ForgotPasswordModel(UserManager<AppUser> userManager, IEmailSender emailSender) { _userManager = userManager; _emailSender = emailSender; } [BindProperty] public InputModel Input { get; set; } public class InputModel { [Required] [EmailAddress] [Display(Name = "Nhập chính xác địa chỉ email")] public string Email { get; set; } } public async Task<IActionResult> OnPostAsync() { if (ModelState.IsValid) { // Tìm user theo email gửi đến var user = await _userManager.FindByEmailAsync(Input.Email); if (user == null || !(await _userManager.IsEmailConfirmedAsync(user))) { return RedirectToPage("./ForgotPasswordConfirmation"); } // Phát sinh Token để reset password // Token sẽ được kèm vào link trong email, // link dẫn đến trang /Account/ResetPassword để kiểm tra và đặt lại mật khẩu var code = await _userManager.GeneratePasswordResetTokenAsync(user); code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); var callbackUrl = Url.Page( "/Account/ResetPassword", pageHandler: null, values: new { area = "Identity", code }, protocol: Request.Scheme); // Gửi email await _emailSender.SendEmailAsync( Input.Email, "Đặt lại mật khẩu", $"Để đặt lại mật khẩu hãy <a href='{callbackUrl}'>bấm vào đây</a>."); // Chuyển đến trang thông báo đã gửi mail để reset password return RedirectToPage("./ForgotPasswordConfirmation"); } return Page(); } } }
ForgotPassword.cshtml
@page @model ForgotPasswordModel @{ ViewData["Title"] = "Quyên mật khẩu?"; } <h1>@ViewData["Title"]</h1> <h4>Nhập email của tài khoản.</h4> <hr /> <div class="row"> <div class="col-md-4"> <form method="post"> <div asp-validation-summary="All" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.Email"></label> <input asp-for="Input.Email" class="form-control" /> <span asp-validation-for="Input.Email" class="text-danger"></span> </div> <button type="submit" class="btn btn-primary">Gửi</button> </form> </div> </div> @section Scripts { <partial name="_ValidationScriptsPartial" /> }
Trang ForgotPasswordConfirmation.cshtml
chỉ đơn giản hiện thị thông báo cho người
dùng biết sau khi email reset password được gửi đi
ForgotPasswordConfirmation.cshtml
@page @model ForgotPasswordConfirmation @{ ViewData["Title"] = "Xác nhận quyên mật khẩu"; } <h1>@ViewData["Title"]</h1> <p> Hãy kiểm tra hòm thư email của bạn để thực hiện bước tiếp theo </p>
Khi người dùng bấm vào đường link lấy lại password trong mail gửi đến, nó sẽ gửi token
đến trang Razor ResetPassword
, nội dung:
ResetPassword.cshtml.cs
namespace Album.Areas.Identity.Pages.Account { [AllowAnonymous] public class ResetPasswordModel : PageModel { private readonly UserManager<AppUser> _userManager; public ResetPasswordModel(UserManager<AppUser> userManager) { _userManager = userManager; } [BindProperty] public InputModel Input { get; set; } public class InputModel { [Required] [EmailAddress] public string Email { get; set; } [Required] [StringLength(100, ErrorMessage = "{0} dài {2} đến {1} ký tự.", MinimumLength = 6)] [DataType(DataType.Password)] [Display(Name = "Mật khẩu")] public string Password { get; set; } [DataType(DataType.Password)] [Display(Name = "Nhập lại mật khẩu")] [Compare("Password", ErrorMessage = "Password phải giống nhau.")] public string ConfirmPassword { get; set; } public string Code { get; set; } } public IActionResult OnGet(string code = null) { if (code == null) { return BadRequest("Mã token không có."); } else { Input = new InputModel { // Giải mã lại code từ code trong url (do mã này khi gửi mail // đã thực hiện Encode bằng WebEncoders.Base64UrlEncode) Code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code)) }; return Page(); } } public async Task<IActionResult> OnPostAsync() { if (!ModelState.IsValid) { return Page(); } // Tìm User theo email var user = await _userManager.FindByEmailAsync(Input.Email); if (user == null) { // Không thấy user return RedirectToPage("./ResetPasswordConfirmation"); } // Đặt lại passowrd chu user - có kiểm tra mã token khi đổi var result = await _userManager.ResetPasswordAsync(user, Input.Code, Input.Password); if (result.Succeeded) { // Chuyển đến trang thông báo đã reset thành công return RedirectToPage("./ResetPasswordConfirmation"); } foreach (var error in result.Errors) { ModelState.AddModelError(string.Empty, error.Description); } return Page(); } } }
ResetPassword.cshtml
@page @model ResetPasswordModel @{ ViewData["Title"] = "Đặt lại mật khẩu"; } <h1>@ViewData["Title"]</h1> <h4>Đặt lại mật khẩu.</h4> <hr /> <div class="row"> <div class="col-md-4"> <form method="post"> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <input asp-for="Input.Code" type="hidden" /> <div class="form-group"> <label asp-for="Input.Email"></label> <input asp-for="Input.Email" class="form-control" /> <span asp-validation-for="Input.Email" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Input.Password"></label> <input asp-for="Input.Password" class="form-control" /> <span asp-validation-for="Input.Password" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Input.ConfirmPassword"></label> <input asp-for="Input.ConfirmPassword" class="form-control" /> <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span> </div> <button type="submit" class="btn btn-primary">Cập nhật</button> </form> </div> </div> @section Scripts { <partial name="_ValidationScriptsPartial" /> }
Sau khi cập nhật mật khẩu mới, nó chuyển hướng đến trang ResetPassword.cshtml
ResetPassword.cshtml
@page @model ResetPasswordConfirmationModel @{ ViewData["Title"] = "Đặt lại mật khẩu"; } <h1>@ViewData["Title"]</h1> <p> Đã đặt lại mặt khẩu. <a asp-page="./Login">Đăng nhập tại đây</a>. </p>
Mã nguồn tham khảo ASP_NET_CORE/Album