- Giới thiệu ASP.NET Identity
- Tạo dự án ASP.NET với Identity
- Định nghĩa lớp Model Identity
- Tạo DbContext từ IdentityDbContext
- Cấu hình đăng ký DbContext
- Đăng ký các các dịch vụ Identity vào hệ thống
- Cấu hình và thêm Identity vào pipeline
- Phát sinh Dababase
- Triển khai và đăng ký dịch vụ IEmailSender
- Phát sinh code (scaffold) Identity
- Thêm LoginPartial vào Layout
- Thêm trang thông báo khi chuyển hướng
- Trang đăng ký user (Register)
- Trang RegisterConfirmation và ConfirmEmail
- Trang Logout
- Tạo trang đăng nhập (Log In)
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:
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
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> <!-- ... -->
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ằngtrue
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ý
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
Đồng thời một email được gửi đi - nội dung email đó dạng:
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
Tài khỏa đã đăng nhập
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" /> }
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