C# cơ bản .NET Core

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.SucceedfalseSignInResult.IsLockedOuttrue

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


Đăng ký nhận bài viết mới