C# cơ bản .NET Core

Giới thiệu ASP.NET Core Identity

Identity trong ASP.NET Core được dùng để quản lý tài khoản người dùng trong ứng dụng. Nó cung cấp các tính năng cần thiết để quản lý tài khoản user (tạo, xóa, sửa), vai trò (role, phân quyền), claim, đăng ký, đăng nhập, reset password ...

claim là thông tin về User. Nó diễn tả các thuộc tính của User nhằm sử dụng trong quá trình xác minh, xác thực (authentication, authorization). Claim lưu thông tin dạng cặp key - value, thông thường là thông tin về ID, email, username

Authentication và Authorization (Xác thực và quyền hạn): Xác thực (Authentication) là quá trình trong đó user cung cấp thông tin xác thực (user, password ...) đã được lưu trong hệ thống ứng dụng (database). Nếu thông tin cung cấp chính xác thì user xác thực (authentication) thành công. Sau đó user mới có thể thi hành các tác vụ được cho phép. Quá trình xác định xem người dùng được pháp làm gì (thực hiện tác vụ gì) đó là Authorization - quyền hạn.

Thư viện .NET Identity cung cấp sẵn code để làm việc với Identity từ trang đăng ký, đăng nhập, quản lý User ... Tất cả các file code này có thể phát sinh đưa vào dự án bằng sử dụng lệnh dotnet aspnet-codegenerator identity

Sau đây sẽ tạo một dự án ví dụ có sử dụng Identity để xác thực, phân quyền ... có sử dụng EF truy vấn thông tin lưu ở MS SQL Server

Tạo dự án ASP.NET với Identity

Tạo ứng dụng Razor Pages, ứng dụng quản lý các Album nhạc, vậy tạo thư mục Album, vào thư mục đó thi hành lệnh:

dotnet new webapp

Thêm các gói để làm việc với Identity, EF, MS SQL Server, phát sinh code ...

dotnet add package System.Data.SqlClient
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Logging.Console

dotnet add package Microsoft.AspNetCore.Identity
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.AspNetCore.Identity.UI
dotnet add package Microsoft.AspNetCore.Authentication
dotnet add package Microsoft.AspNetCore.Http.Abstractions
dotnet add package Microsoft.AspNetCore.Authentication.Cookies
dotnet add package Microsoft.AspNetCore.Authentication.Facebook
dotnet add package Microsoft.AspNetCore.Authentication.Google
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Microsoft.AspNetCore.Authentication.MicrosoftAccount
dotnet add package Microsoft.AspNetCore.Authentication.oAuth
dotnet add package Microsoft.AspNetCore.Authentication.OpenIDConnect
dotnet add package Microsoft.AspNetCore.Authentication.Twitter

Cũng cài đặt công cụ về EF nếu chưa cài đặt:

dotnet tool install --global dotnet-ef

Chuẩn bị máy chủ Cơ sở dữ liệu SQL SERVER: bạn cần có một máy chủ cơ sở dữ liệu (giả sử MS SQL Server), nếu chưa hãy tạo máy chủ này theo hướng dẫn MS SQL Server Docker

Khi đã có máy chủ, cần thêm chuỗi kết nối vào cấu hình ứng dụng ở file appsettings.json (xem thêm chuỗi kết nối CSDL), giả sử database đặt tên là albumdb, thì chuỗi kết nối cần thêm vào cấu hình có thể là

"ConnectionStrings": {
    "AppDbContext": "Data Source=localhost,1433; Initial Catalog=albumdb; User ID=SA;Password=Password123"
}

Chuỗi kết nối này sẽ được DbContext mà chúng ta sẽ tạo ra sau đây sủ dụng kết nối đến database.

Tạo Model Identity

ASP.NET cung cấp sẵn một lớp Model chứa các thông tin về User, đó là lớp IdentityUser nó kế thừa từ lớp generic Microsoft.AspNetCore.Identity.IdentityUser<TKey> với kiểu xác định string. Lớp IdentityUser đã định nghĩa sẵn các thuộc tính, khai báo có dạng tượng tự như sau:

public class IdentityUser
{
    public IdentityUser(string userName);
    public virtual bool TwoFactorEnabled { get; set; }
    public virtual bool PhoneNumberConfirmed { get; set; }
    public virtual string PhoneNumber { get; set; }
    public virtual string SecurityStamp { get; set; }
    public virtual bool EmailConfirmed { get; set; }
    public virtual string Email { get; set; }
    public virtual string UserName { get; set; }
    public virtual string Id { get; set; }
    public virtual bool LockoutEnabled { get; set; }
    public virtual int AccessFailedCount { get; set; }
}

Ta có thể dùng luôn IdentityUser, tuy nhiên để bổ sung các thuộc tinh ta sẽ khai báo lớp Model kế thừa từ IdentityUser chứ không dùng trực tiếp, tạo lớp AppUser như sau:

Models/AppUser.cs

using Microsoft.AspNetCore.Identity;

namespace Album.Models
{
    public class AppUser : IdentityUser
    { 
        // Khai báo thêm các thuộc tính ngoài các thuộc
        // tính như UserName, Email ... cung cấp sẵn bởi IdentityUser
    }
}

Ngoài lớp IdentityUser, có những lớp sau cũng được sử dụng: IdentityRole, IdentityUserRole , IdentityUserClaim , IdentityUserLogin , IdentityUserToken , IdentityRoleClaim , những lớp này sẽ có trong DbContext của ứng dụng.

Tạo DbContext từ IdentityDbContext

DbContext biểu diễn CSDL (cần nắm vững về DbContext trước tại DbContext trong EF ), trong đó lớp IdentityDbContext là DbContext định nghĩa sẵn trong nó có sẵn các DbSet như:

  • UserRoles
  • Roles
  • RoleClaims
  • Users
  • UserClaims
  • UserLogins
  • UserTokens

Đây chính là các bảng của CSDL chứa thông tin về Identity, để tạo ra DbContext riêng, bạn chỉ việc kế thừa lớp IdentityDbContext. Ta xây dựng DbContext là lớp AppDbContext

using Album.Models;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace Album.Data {
    
    // Kế thừa từ IdentityDbContext nên có sẵn các DbSet
    // UserRoles Roles RoleClaimsUsers UserClaims UserLogins UserTokens
    public class AppDbContext : IdentityDbContext<AppUser> {

        public AppDbContext (DbContextOptions<AppDbContext> options) : base (options) { }

        protected override void OnModelCreating (ModelBuilder builder) {

            base.OnModelCreating (builder); 
            // Bỏ tiền tố AspNet của các bảng: mặc định các bảng trong IdentityDbContext có
            // tên với tiền tố AspNet như: AspNetUserRoles, AspNetUser ...
            // Đoạn mã sau chạy khi khởi tạo DbContext, tạo database sẽ loại bỏ tiền tố đó
            foreach (var entityType in builder.Model.GetEntityTypes ()) {
                var tableName = entityType.GetTableName ();
                if (tableName.StartsWith ("AspNet")) {
                    entityType.SetTableName (tableName.Substring (6));
                }
            }
        }

    }
}

Tiếp theo là đăng ký AppDbContext vào dịch vụ hệ thống, xem Đăng ký DbContext vào ServiceCollection

Đăng ký AppDbContext vào dịch vụ hệ thống

Cách đăng ký xem tại đăng ký DbContext vào hệ thống, mở file Startup.cs, sửa phương thức ConfigureServices

public void ConfigureServices(IServiceCollection services)
{

    // Đăng ký AppDbContext
    services.AddDbContext<AppDbContext>(options => {
        // Đọc chuỗi kết nối
        string connectstring = Configuration.GetConnectionString("AppDbContext");
        // Sử dụng MS SQL Server
        options.UseSqlServer(connectstring);
    });

    services.AddRazorPages();
}

Đăng ký các các dịch vụ Identity vào hệ thống

Trong ConfigureServices cho thêm vào, trở thành

public void ConfigureServices(IServiceCollection services)
{

    // Đăng ký AppDbContext
    services.AddDbContext<AppDbContext>(options => {
        // Đọc chuỗi kết nối
        string connectstring = Configuration.GetConnectionString("AppDbContext");
        // Sử dụng MS SQL Server
        options.UseSqlServer(connectstring);
    });

    services.AddIdentity<AppUser, IdentityRole>()
        .AddEntityFrameworkStores<AppDbContext>()
        .AddDefaultTokenProviders();

    services.AddRazorPages();
}

Nếu viết chi tiết ra:

// Thêm vào dịch vụ Identity với cấu hình mặc định cho AppUser (model user) vào IdentityRole (model Role - vai trò)
var identityservice = services.AddIdentity<AppUser, IdentityRole>();

// Thêm triển khai EF lưu trữ thông tin về Idetity (theo AppDbContext -> MS SQL Server).
identityservice.AddEntityFrameworkStores<<AppDbContext>();

// Thêm Token Provider - nó sử dụng để phát sinh token (reset password, confirm email ...)
// đổi email, số điện thoại ...
identityservice.AddDefaultTokenProviders();

Thêm Identity vào pipeline, cấu hình Identity

Sửa phương thức Configure trong Startup.cs, thêm vào dòng sau (thêm sau Routing) nếu chưa có:

app.UseAuthentication();   // Phục hồi thông tin đăng nhập (xác thực)
app.UseAuthorization ();   // Phục hồi thông tinn về quyền của User

Dòng trên thêm vào pipeline middleware AuthorizationMiddleware, cung cấp chức năng xác định quyền. Nó cung cấp chức năng lưu thông tin khi người dùng truy cập, thông tin lưu tại HttpContext.User

Nếu muốn thiết lập thay đổi cấu hình mặc định của Identity thì thay đổi dịch vụ cấu hình của nó như sau, trong ConfigureServices của lớp StartUp thêm đoạn mã cấu hình (nhớ thêm phía sau AddIdentity):

// Truy cập IdentityOptions
services.Configure<IdentityOptions> (options => {
    // Thiết lập về Password
    options.Password.RequireDigit = false; // Không bắt phải có số
    options.Password.RequireLowercase = false; // Không bắt phải có chữ thường
    options.Password.RequireNonAlphanumeric = false; // Không bắt ký tự đặc biệt
    options.Password.RequireUppercase = false; // Không bắt buộc chữ in
    options.Password.RequiredLength = 3; // Số ký tự tối thiểu của password
    options.Password.RequiredUniqueChars = 1; // Số ký tự riêng biệt

    // Cấu hình Lockout - khóa user
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes (5); // Khóa 5 phút
    options.Lockout.MaxFailedAccessAttempts = 5; // Thất bại 5 lầ thì khóa
    options.Lockout.AllowedForNewUsers = true;

    // Cấu hình về User.
    options.User.AllowedUserNameCharacters = // các ký tự đặt tên user
        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
    options.User.RequireUniqueEmail = true;  // Email là duy nhất

    // Cấu hình đăng nhập.
    options.SignIn.RequireConfirmedEmail = true;            // Cấu hình xác thực địa chỉ email (email phải tồn tại)
    options.SignIn.RequireConfirmedPhoneNumber = false;     // Xác thực số điện thoại

});

Phát sinh Database có các table về Identity

Khi đã có DbContext là AppDbContext bạn có thể sử dụng kỹ thuật migration trong EF để phát sinh, cập nhật DB - xem chi tiết tại Migration trong EntityFramework

Để ý là trong AppDbContext đã dùng kỹ thuật Entity Framework với Fluent API để đổi tên bảng mặc định (loại bỏ tiền tố AspNet)

Chạy lệnh để tạo Migration và tạo db

dotnet ef migrations add Init
dotnet ef database update

Sau các lệnh này đã có Db, và hệ thống bắt đầu có thể làm việc với các dịch vụ Identity. Cơ sở dữ liệu ban đầu này tạo ra gồm có có bảng và có sơ đồ quan hệ như sau:

identity01

Triển khai và đăng ký dịch vụ IEmailSender

Phần sau ta sẽ sử dụng lệnh dotnet aspnet-codegenerator identity để phát sinh ra code dựng sẵn về các trang Razor Page (hoặc các Controller) của Identity từ đó ta chỉ việc tùy biến thay vì phải code từ đầu. Trong chức năng của Identity có nhiều chỗ sử dụng dịch vụ ImailSender để gửi mail, ví dụ khi người dùng đăng ký thì gửi mail kích hoạt, người dùng reset password gửi mail để xác thực ...

Do vậy ta cần triển khai một lớp từ giao diện IEmailSender sau đó đăng ký vào hệ thống để Identity cần thì gọi dịch vụ này và sử dụng, xây dựng lớp đó như sau (áp dụng lại ví dụ tại Dùng MailKit gửi Mail trong ASP.NET với Gmail ):

Thêm vào các gói

dotnet add package MailKit
dotnet add package MimeKit

Thêm vào appsettings.json

{

  // các mục khác của config

  "MailSettings": {
    "Mail": "gmail của bạn",
    "DisplayName": "Tên Hiện Thị (ví dụ XUANTHULAB)",
    "Password": "passowrd ở đây",
    "Host": "smtp.gmail.com",
    "Port": 587
  }
}

Xây dựng các lớp MailSettings, SendMailService như sau:

Mail/SendMailService.cs

using System;
using System.Threading.Tasks;
using MailKit.Security;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MimeKit;

namespace Album.Mail {

    // Cấu hình dịch vụ gửi mail, giá trị Inject từ appsettings.json
    public class MailSettings {
        public string Mail { get; set; }
        public string DisplayName { get; set; }
        public string Password { get; set; }
        public string Host { get; set; }
        public int Port { get; set; }

    }

    // Dịch vụ gửi mail
    public class SendMailService : IEmailSender {
        private readonly MailSettings mailSettings;

        private readonly ILogger<SendMailService> logger;

        // mailSetting được Inject qua dịch vụ hệ thống
        // Có inject Logger để xuất log
        public SendMailService (IOptions<MailSettings> _mailSettings, ILogger<SendMailService> _logger) {
            mailSettings = _mailSettings.Value;
            logger = _logger;
            logger.LogInformation ("Create SendMailService");
        }

        public async Task SendEmailAsync (string email, string subject, string htmlMessage) {
            var message = new MimeMessage ();
            message.Sender = new MailboxAddress (mailSettings.DisplayName, mailSettings.Mail);
            message.From.Add (new MailboxAddress (mailSettings.DisplayName, mailSettings.Mail));
            message.To.Add (MailboxAddress.Parse (email));
            message.Subject = subject;

            var builder = new BodyBuilder ();
            builder.HtmlBody = htmlMessage;
            message.Body = builder.ToMessageBody ();

            // dùng SmtpClient của MailKit
            using var smtp = new MailKit.Net.Smtp.SmtpClient ();

            try {
                smtp.Connect (mailSettings.Host, mailSettings.Port, SecureSocketOptions.StartTls);
                smtp.Authenticate (mailSettings.Mail, mailSettings.Password);
                await smtp.SendAsync (message);
            } catch (Exception ex) {
                // Gửi mail thất bại, nội dung email sẽ lưu vào thư mục mailssave
                System.IO.Directory.CreateDirectory ("mailssave");
                var emailsavefile = string.Format (@"mailssave/{0}.eml", Guid.NewGuid ());
                await message.WriteToAsync (emailsavefile);

                logger.LogInformation ("Lỗi gửi mail, lưu tại - " + emailsavefile);
                logger.LogError (ex.Message);
            }

            smtp.Disconnect (true);

            logger.LogInformation ("send mail to: " + email);

        }
    }
}

Tiến hành đăng ký dịch vụ SendMailService và hệ thống với tên giao diện IEmailSender, thêm vào ConfigureServices của Startup đoạn code:

services.AddOptions ();                                        // Kích hoạt Options
var mailsettings = Configuration.GetSection ("MailSettings");  // đọc config
services.Configure<MailSettings> (mailsettings);               // đăng ký để Inject

services.AddTransient<IEmailSender, SendMailService>();        // Đăng ký dịch vụ Mail

Từ giờ chỗ nào cần sử dụng IEmailSender để gửi mail thì đã có sẵn trong hệ thống.

Phát sinh code (scaffold) Identity

Lệnh dotnet aspnet-codegenerator identity sẽ phát sinh các code (các Model, trang Razor Page, Controller, View) sẵn cho chúng ta, qua đó chỉ việc tùy biến lại, bạn thực hiện lệnh như sau (nhớ cài đặt đầy đủ công cụ và các gói ở trên):

dotnet aspnet-codegenerator identity -dc Album.Data.AppDbContext

Tham số -dc Album.Data.AppDbContext để chỉ ra là Identity sử dụng DbContext là Album.Data.AppDbContext của chúng ta xây dựng ở trên

Vì ứng dụng khởi tạo ở trên là ASP.NET Razor Pages, nên sau lệnh này nó phát sinh cho dự án sẵn dùng được ngay một loạt trang Razor, mỗi trang tương ứng với các chức năng của Identity như Đăng ký, đăng nhập ... Các trang này được bố trí trong Areas có tên Identity - trong thư mục Areas/Identity/Pages

identity01-album05

Bạn truy cập Url theo cấu trúc Route của Razor Page (xem Routing đến trang Razor Page trong thư mục Pages và Area) như url: https://localhost:5001/Identity/Account/Register, https://localhost:5001/Identity/Account/Login ... Bạn sẽ thấy các trang này đã làm việc, bạn đã có đầy đủ tính năng của Identity - Giờ chỉ cần tùy biến theo nhu cầu của bạn.

Thêm _LoginPartial.cshtml vào Layout

Lênh phát sinh code Identity cũng phát sinh ra một Partial tại Pages/Shared/_LoginPartial.cshtml với nội dung như sau:

@using Microsoft.AspNetCore.Identity
@using Album.Models

@inject SignInManager<AppUser> SignInManager
@inject UserManager<AppUser> UserManager

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
    <li class="nav-item">
        <a id="manage" class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @UserManager.GetUserName(User)!</a>
    </li>
    <li class="nav-item">
        <form id="logoutForm" class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/Index", new { area = "" })">
            <button id="logout" type="submit" class="nav-link btn btn-link text-dark">Logout</button>
        </form>
    </li>
}
else
{
    <li class="nav-item">
        <a class="nav-link text-dark" id="register" asp-area="Identity" asp-page="/Account/Register">Đăng ký</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" id="login" asp-area="Identity" asp-page="/Account/Login">Đăng nhập</a>
    </li>
}
</ul>

Đoạn mã .cshtml này tạo ra cấu trúc HTML navbar-nav của Bootstrap hiện thị menu trên về thông tin người dùng đăng nhập, link bấm vào để đăng xuất hoặc đăng nhập.

Hãy đưa _LoginPartial.cshtml vào _Layout.cshtml để tất cả các trang đều xuất hiện menu này. Mở _Layout.cshtml thêm vào @await Html.PartialAsync("_LoginPartial"), thêm dúng vị trí nav:

<!-- ...   -->
<header>
    <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
        <div class="container">
            <!--  nội dung khác  -->
            @await Html.PartialAsync("_LoginPartial")
        </div>
    </nav>
</header>
<!-- ...   -->
identity01-album05

Một số lưu ý về code trong _LoginPartial.cshtml:

@inject SignInManager<AppUser> SignInManager
@inject UserManager<AppUser> UserManager

Hai dòng trên để Inject hai dịch vụ của Identity là UserManger (quản lý User: thêm, xóa, sửa ...) và SignnInManager (quản lý đăng nhập) vào Razor Page.

Trong Controller, PageModel, View, Razor Page đều có thuộc tính User kiểu ClaimsPrincipal là mẩu tin về User của phiên làm việc, để kiểm tra có User đăng nhập thì gọi:

SignInManager.IsSignedIn(User)

Để lấy tên UserName của User của phiên làm việc:

UserManager.GetUserName(User)

Thêm trang thông báo khi chuyển hướng

Trong nhiều trường hợp khi hoàn thành một tác vụ thì chuyển hướng đến trang đích nào đó, ví dụ sau khi đăng ký xong tài khoản thì chuyển về trang chủ. Ở đây xây dựng thêm chức năng hiện thị một thống báo rồi mới chuyển hướng, ví dụ thông báo cho biết đã đăng ký tài khỏan thành công - nhưng cần xác nhận địa chỉ email, thông báo hiện thị vài giây rồi chuyển hướng.

Hãy copy mã nguồn ViewComponent có tên MessagePage tạo trang thông báo tại Trang thông báo + chuyển hướng vào dự án để sử dụng ngay

Trong các Action nào muốn chuyển hướng, chỉ việc gọi:

return ViewComponent("MessagePage", new XTLASPNET.MessagePage.Message {
    title = "TIÊU ĐỀ",
    htmlcontent = "Nội dung thông báo",
    secondwait = 5,
    urlredirect = "url-chuyển-hướng-đến"
});

Các cấu hình và thiết lập cơ bản đã xong, giờ chỉ việc tùy biến theo nhu cầu

Trang đăng ký tài khoản User - Identity

Nội dung của trang này phát sinh tại Areas/Identity/Pages/Account/Register.cs[html] truy cập qua địa chỉ /identity/account/register, trước tiên cấu hình để địa chỉ url ngắn gọn là /register/, mở Register.cshtml sửa dòng @page thành

@page "/register/"

Mặc định trang đăng ký này yêu cầu thông tin gồm email và password - email vừa cập nhật vào email của user và cũng dùng làm UserName. Ta sẽ cấu hình cần form nhập cả UserName nữa. Có sẽ như sau, các vấn đề giải thích trong comment của code

Register.cshtml.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Album.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;

namespace Album.Areas.Identity.Pages.Account
{
    [AllowAnonymous]
    public class RegisterModel : PageModel
    {
        private readonly SignInManager<AppUser> _signInManager;
        private readonly UserManager<AppUser> _userManager;
        private readonly ILogger<RegisterModel> _logger;
        private readonly IEmailSender _emailSender;

        // Các dịch vụ được Inject vào: UserManger, SignInManager, ILogger, IEmailSender
        public RegisterModel(
            UserManager<AppUser> userManager,
            SignInManager<AppUser> signInManager,
            ILogger<RegisterModel> logger,
            IEmailSender emailSender)
        {
            _userManager = userManager;
            _signInManager = signInManager;
            _logger = logger;
            _emailSender = emailSender;
        }

        // InputModel được binding khi Form Post tới

        [BindProperty]
        public InputModel Input { get; set; }

        public string ReturnUrl { get; set; }

        // Xác thực từ dịch vụ ngoài (Googe, Facebook ... bài này chứa thiết lập)
        public IList<AuthenticationScheme> ExternalLogins { get; set; }

        // Lớp InputModel chứa thông tin Post tới dùng để tạo User
        public class InputModel
        {
            [Required]
            [EmailAddress]
            [Display(Name = "Địa chỉ Email")]
            public string Email { get; set; }

            [Required]
            [StringLength(100, ErrorMessage = "{0} dài từ {2} đến {1} ký tự.", MinimumLength = 3)]
            [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 = "Mật khẩu không giống nhau")]
            public string ConfirmPassword { get; set; }

            [Required]
            [StringLength(100, ErrorMessage = "{0} dài từ {2} đến {1} ký tự.", MinimumLength = 3)]
            [DataType(DataType.Text)]
            [Display(Name="Tên tài khoản (viết liền - không dấu)")]
            public string UserName {set; get;}
        }

        public async Task OnGetAsync(string returnUrl = null)
        {
            ReturnUrl = returnUrl;
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
        }
        
        // Đăng ký tài khoản theo dữ liệu form post tới
        public async Task<IActionResult> OnPostAsync(string returnUrl = null)
        {
            returnUrl = returnUrl ?? Url.Content("~/");
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
            if (ModelState.IsValid)
            {
                // Tạo AppUser sau đó tạo User mới (cập nhật vào db)
                var user = new AppUser { UserName = Input.UserName, Email = Input.Email };
                var result = await _userManager.CreateAsync(user, Input.Password);

                if (result.Succeeded)
                {
                    _logger.LogInformation("Vừa tạo mới tài khoản thành công.");

                    // phát sinh token theo thông tin user để xác nhận email
                    // mỗi user dựa vào thông tin sẽ có một mã riêng, mã này nhúng vào link
                    // trong email gửi đi để người dùng xác nhận
                    var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                    code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));

                    // callbackUrl = /Account/ConfirmEmail?userId=useridxx&code=codexxxx
                    // Link trong email người dùng bấm vào, nó sẽ gọi Page: /Acount/ConfirmEmail để xác nhận
                    var callbackUrl = Url.Page(
                        "/Account/ConfirmEmail",
                        pageHandler: null,
                        values: new { area = "Identity", userId = user.Id, code = code, returnUrl = returnUrl },
                        protocol: Request.Scheme);

                    // Gửi email    
                    await _emailSender.SendEmailAsync(Input.Email, "Xác nhận địa chỉ email",
                        $"Hãy xác nhận địa chỉ email bằng cách <a href='{callbackUrl}'>Bấm vào đây</a>.");

                    if (_userManager.Options.SignIn.RequireConfirmedEmail)
                    {
                        // Nếu cấu hình phải xác thực email mới được đăng nhập thì chuyển hướng đến trang
                        // RegisterConfirmation - chỉ để hiện thông báo cho biết người dùng cần mở email xác nhận
                        return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
                    }
                    else
                    {
                        // Không cần xác thực - đăng nhập luôn
                        await _signInManager.SignInAsync(user, isPersistent: false);
                        return LocalRedirect(returnUrl);
                    }
                }
                // Có lỗi, đưa các lỗi thêm user vào ModelState để hiện thị ở html heleper: asp-validation-summary
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }

            return Page();
        }
    }
}

Một số lưu ý của code trên: Hai dịch vụ của Identity được sử dụng là UserManager và UserSignIn được Inject vào PageModel qua phương thức khởi tạo

  • Thuộc tính [AllowAnonymous] để xác nhận quyền người dùng không đăng nhập vẫn được quyền truy cập Page đó.
  • Trong InputModel đã thêm vào UserName để xây dựng Form cần nhập cả Email và UserName
  • Để tạo mới một User dùng phương thức UserManager.CreateAsync(user, password); nó trả về IdentityResult, nếu IdentityResult.Succeeded bằng true thì đã thêm user thành công
  • UserManager.GenerateEmailConfirmationTokenAsync để phát sinh một mã Token duy nhất theo thông tin của User, mã này được dùng để xác nhận email đăng ký là có thật.
  • Nếu có đối tượng IdentityUser thì có thể đăng nhập bằng phương thức UserManager.SignInAsync(user, isPersistent: false)

Nội dung trang View của Register sửa lại thành như sau

Register.cshtml

@page "/register/"
@model RegisterModel
@{
    ViewData["Title"] = "ĐĂNG KÝ TÀI KHOẢN";
}

<h1>@ViewData["Title"]</h1>

<div class="row">
    <div class="col-md-6">
        <form asp-route-returnUrl="@Model.ReturnUrl" method="post">
            <h4>Điền thông tin để tạo tài khoản mới.</h4>
            <hr />
            <div asp-validation-summary="All" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Input.UserName"></label>
                <input asp-for="Input.UserName" class="form-control" />
                <span asp-validation-for="Input.UserName" class="text-danger"></span>
            </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>
            <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">Đăng ký</button>
        </form>
    </div>
    <div class="col-md-6 col-md-offset-2">
        <section>
            <h4>Đăng ký bằng sử dụng các dịch vụ ngoài</h4>
            <hr />
            @{
                if ((Model.ExternalLogins?.Count ?? 0) == 0)
                {
                    <div>
                        <p>
                            Phần này chưa cấu hình đăng ký từ dịch vụ ngoài. 
                            See <a href="https://go.microsoft.com/fwlink/?LinkID=532715">this article</a>
                        </p>
                    </div>
                }
                else
                {
                    <form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
                        <div>
                            <p>
                                @foreach (var provider in Model.ExternalLogins)
                                {
                                    <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
                                }
                            </p>
                        </div>
                    </form>
                }
            }
        </section>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

Với trang này thì đã có thể đăng ký tài khoản vào hệ thống, tuy nhiên để hoàn chỉnh cập nhật thêm cả các trang RegisterConfirmation, ConfirmEmail

Trang RegisterConfirmation và ConfirmEmail

RegisterConfirmation

Trang này chỉ có chức năng hiện thị thông báo cần kích hoạt email, sau khi người dùng đăng ký, cập nhật thành

RegisterConfirmation.cshtml.cs

using Microsoft.AspNetCore.Authorization;
using System.Text;
using System.Threading.Tasks;
using Album.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
using XTLASPNET;

namespace Album.Areas.Identity.Pages.Account
{
    [AllowAnonymous]
    public class RegisterConfirmationModel : PageModel
    {
        private readonly UserManager<AppUser> _userManager;
        public RegisterConfirmationModel(UserManager<AppUser> userManager)
        {
            _userManager = userManager;
        }

        public string Email { get; set; }

        public string UrlContinue {set; get;}


        public async Task<IActionResult> OnGetAsync(string email, string returnUrl = null)
        {
            if (email == null)
            {
                return RedirectToPage("/Index");
            }


            var user = await _userManager.FindByEmailAsync(email);
            if (user == null)
            {
                return NotFound($"Không có user với email: '{email}'.");
            }

            if (user.EmailConfirmed) {
                // Tài khoản đã xác thực email
                return ViewComponent(MessagePage.COMPONENTNAME,
                        new MessagePage.Message() {
                            title = "Thông báo",
                            htmlcontent = "Tài khoản đã xác thực, chờ chuyển hướng",
                            urlredirect = (returnUrl != null) ? returnUrl : Url.Page("/Index")
                        }

                );    
            }

            Email = email;

            if (returnUrl != null) {
                UrlContinue  =  Url.Page("RegisterConfirmation", new { email = Email, returnUrl = returnUrl });
            }
            else 
                UrlContinue  =  Url.Page("RegisterConfirmation", new { email = Email });


            return Page();
        }
    }
}

RegisterConfirmation.cshtml

@page "/RegisterConfirmation/"
@model RegisterConfirmationModel
@{
    ViewData["Title"] = "Xác nhận đăng ký";
}

<h1>@ViewData["Title"]</h1>
<p>Một email đã gửi đến cho bạn, bạn cần mở email làm theo hướng dẫn trong email sau đó 
    <a href="@Model.UrlContinue">Bấm vào đây để tiếp tục</a>
</p>

Trang ConfirmEmail

Trang này dùng để chứng thực email đăng ký của người dùng là thật, nó nhận thông tin là userid và mã token trong url truy cập, dùng hai thông tin này để xác thực, code sửa có dạng như sau:

ConfirmEmail.cshtml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Album.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
using XTLASPNET;

namespace Album.Areas.Identity.Pages.Account {
    [AllowAnonymous]
    public class ConfirmEmailModel : PageModel {
        private readonly UserManager<AppUser> _userManager;
        private readonly SignInManager<AppUser> _signInManager;

        public ConfirmEmailModel (UserManager<AppUser> userManager, SignInManager<AppUser> signInManager) {
            _userManager = userManager;
            _signInManager = signInManager;
        }

        [TempData]
        public string StatusMessage { get; set; }

        public async Task<IActionResult> OnGetAsync (string userId, string code, string returnUrl) {
                        
            if (userId == null || code == null) {
                return RedirectToPage ("/Index");
            }


            var user = await _userManager.FindByIdAsync (userId);
            if (user == null) {
                return NotFound ($"Không tồn tại User - '{userId}'.");
            }

            code = Encoding.UTF8.GetString (WebEncoders.Base64UrlDecode (code));
            // Xác thực email
            var result = await _userManager.ConfirmEmailAsync (user, code);

            if (result.Succeeded) {
                
                // Đăng nhập luôn nếu xác thực email thành công
                await _signInManager.SignInAsync(user, false);

                return ViewComponent (MessagePage.COMPONENTNAME,
                    new MessagePage.Message () {
                        title = "Xác thực email",
                            htmlcontent = "Đã xác thực thành công, đang chuyển hướng",
                            urlredirect = (returnUrl != null) ? returnUrl : Url.Page ("/Index")
                    }
                );
            } else {
                StatusMessage = "Lỗi xác nhận email";
            }
            return Page ();
        }
    }
}

ConfirmEmail.cshtml

@page "/confirm-email/"
@model ConfirmEmailModel
@{
    ViewData["Title"] = "Xác thực email";
}

<h1>@ViewData["Title"]</h1>
<p>@Model.StatusMessage</p>

Đăng ký thử User

Điền thông tin và đăng ký

identity01

Sau khi bấm gửi, nếu dữ liệu đúng - User tạo thành công thì nó chuyển hướng đến

identity01

Đồng thời một email được gửi đi - nội dung email đó dạng:

identity01

Người dùng bấm vào link trong email để chứng thực email, nó chuyển đến trang để chứng thực - nếu thành công thì tự động đăng nhập luônn và có thông báo

identity01

Tài khỏa đã đăng nhập

identity01

Trang Log Out

Trang Logout để User đang nhập xác nhận tiến hành đăng xuất (Logout). Để đăng xuất User của phiên làm việc đơn giản gọi await SignInManager.SignOutAsync(). Ta sửa trang Logout như sau:

Logout.cshtml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Album.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using XTLASPNET;

namespace Album.Areas.Identity.Pages.Account
{
    [AllowAnonymous]
    public class LogoutModel : PageModel
    {
        private readonly SignInManager<AppUser> _signInManager;
        private readonly ILogger<LogoutModel> _logger;

        public LogoutModel(SignInManager<AppUser> signInManager, ILogger<LogoutModel> logger)
        {
            _signInManager = signInManager;
            _logger = logger;
        }

        public async Task<IActionResult> OnPost(string returnUrl = null)
        {
            if (!_signInManager.IsSignedIn(User)) return RedirectToPage("/Index");

            await _signInManager.SignOutAsync();
            _logger.LogInformation("Người dùng đăng xuất");


            return ViewComponent(MessagePage.COMPONENTNAME, 
                new MessagePage.Message() {
                    title = "Đã đăng xuất",
                    htmlcontent = "Đăng xuất thành công",
                    urlredirect = (returnUrl != null) ? returnUrl : Url.Page("/Index")
                }
            );
        }
    }
}

Logout.cshtml

@page "/logout/"
@model LogoutModel
@{
    ViewData["Title"] = "Log out";
}

Thực tế không dùng đến Logout.cshtml để Post xác nhận mà việc Post đăng xuất thực hiện trong _LoginPartial.cshtml ở đoạn code

<form id="logoutForm" class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/Index", new { area = "" })">
    <button id="logout" type="submit" class="nav-link btn btn-link text-dark">Đăng xuất</button>
</form>

Như vậy nếu người dùng đăng nhấp, bấm vào Đăng xuất ở menu phải sẽ đăng xuất.

Trang Log In

Trang Login để người dùng đăng nhập. Ta tùy biến để người dùng nhập UserName hoặc Email và Password để đăng nhập. Toàn bộ code như sau:

Login.cshtml.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Album.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using XTLASPNET;

namespace Album.Areas.Identity.Pages.Account {
    [AllowAnonymous]
    public class LoginModel : PageModel {
        private readonly UserManager<AppUser> _userManager;
        private readonly SignInManager<AppUser> _signInManager;
        private readonly ILogger<LoginModel> _logger;

        public LoginModel (SignInManager<AppUser> signInManager,
            ILogger<LoginModel> logger,
            UserManager<AppUser> userManager) {
            _userManager = userManager;
            _signInManager = signInManager;
            _logger = logger;
        }

        [BindProperty]
        public InputModel Input { get; set; }

        public IList<AuthenticationScheme> ExternalLogins { get; set; }

        public string ReturnUrl { get; set; }

        [TempData]
        public string ErrorMessage { get; set; }

        public class InputModel {
            [Required (ErrorMessage = "Không để trống")]
            [Display (Name = "Nhập username hoặc email của bạn")]
            [StringLength (100, MinimumLength = 1, ErrorMessage = "Nhập đúng thông tin")]
            public string UserNameOrEmail { set; get; }

            [Required]
            [DataType (DataType.Password)]
            [Display(Name = "Mật khẩu")]
            public string Password { get; set; }

            [Display (Name = "Nhớ thông tin đăng nhập?")]
            public bool RememberMe { get; set; }
        }

        public async Task OnGetAsync (string returnUrl = null) {
            if (!string.IsNullOrEmpty (ErrorMessage)) {
                ModelState.AddModelError (string.Empty, ErrorMessage);
            }

            returnUrl = returnUrl ?? Url.Content ("~/");

            // Clear the existing external cookie to ensure a clean login process
            await HttpContext.SignOutAsync (IdentityConstants.ExternalScheme);

            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync ()).ToList ();

            ReturnUrl = returnUrl;
        }

        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) {
                // Thử login bằng username/password
                var result = await _signInManager.PasswordSignInAsync (
                    Input.UserNameOrEmail,
                    Input.Password,
                    Input.RememberMe,
                    true
                );

                if (!result.Succeeded) {
                    // Thất bại username/password -> tìm user theo email, nếu thấy thì thử đăng nhập
                    // bằng user tìm được
                    var user = await _userManager.FindByEmailAsync (Input.UserNameOrEmail);
                    if (user != null) {
                        result = await _signInManager.PasswordSignInAsync (
                            user,
                            Input.Password,
                            Input.RememberMe,
                            true
                        );
                    }
                }

                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 ();
                }
            }

            // If we got this far, something failed, redisplay form
            return Page ();
        }
    }
}

Login.cshtml

@page "/login/"
@model LoginModel

@{
    ViewData["Title"] = "ĐĂNG NHẬP";
}

<h1>@ViewData["Title"]</h1>
<div class="row">
    <div class="col-md-4">
        <section>
            <form id="account" method="post">
                <h4>Điền thông tin để đăng nhập.</h4>
                <hr />
                <div asp-validation-summary="All" class="text-danger"></div>
                <div class="form-group">
                    <label asp-for="Input.UserNameOrEmail"></label>
                    <input asp-for="Input.UserNameOrEmail" class="form-control" />
                    <span asp-validation-for="Input.UserNameOrEmail" 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">
                    <div class="checkbox">
                        <label asp-for="Input.RememberMe">
                            <input asp-for="Input.RememberMe" />
                            @Html.DisplayNameFor(m => m.Input.RememberMe)
                        </label>
                    </div>
                </div>
                <div class="form-group">
                    <button type="submit" class="btn btn-primary">Đăng nhập</button>
                </div>
                <div class="form-group">
                    <p>
                        <a id="forgot-password" asp-page="./ForgotPassword">Bạn quyên mật khẩu?</a>
                    </p>
                    <p>
                        <a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">Chưa có tài khoản, đăng ký mới</a>
                    </p>
                </div>
            </form>
        </section>
    </div>
    <div class="col-md-6 col-md-offset-2">
        <section>
            <h4>Đăng nhập bằng.</h4>
            <hr />
            @{
                if ((Model.ExternalLogins?.Count ?? 0) == 0)
                {
                    <div>
                        <p>
                            Chưa cấu hình đăng nhập bằng dịch vụ khác. See <a href="https://go.microsoft.com/fwlink/?LinkID=532715">this article</a>
                        </p>
                    </div>
                }
                else
                {
                    <form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
                        <div>
                            <p>
                                @foreach (var provider in Model.ExternalLogins)
                                {
                                    <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
                                }
                            </p>
                        </div>
                    </form>
                }
            }
        </section>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}
identity01

Mã nguồn tham khảo ASP_NET_CORE/Album hoặc tải về phiên bản của bài này ex063


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