Bài 92: (ASP.NET Core MVC) Tích hợp Entity Framework và Identity

Ngày đăng: 12/30/2022 10:43:24 AM

ASP.NET MVC với Entity Framework làm việc với SQL Server

Việc tích hợp Entity Framework vào APS.NET MVC thực hoàn hoàn toàn giống với các bài đã hướng dẫn ở Razor Page - quy trình thực hiện theo các bước tại (ASP.NET Razor) Ứng dụng EF làm việc với cơ sở dữ liệu

Thực hiện thêm các package để làm việc với EF kết nối đến MS SQL Server và các công cụ trợ giúp phát sinh code:

 dotnet tool install --global dotnet-ef
 dotnet tool install --global dotnet-aspnet-codegenerator
 dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
 dotnet add package Microsoft.EntityFrameworkCore.Design
 dotnet add package Microsoft.EntityFrameworkCore.SqlServer
 dotnet add package MySql.Data.EntityFramework
 

Nếu chưa có máy chủ MS SQL Server có thể làm theo hướng dẫn: chuẩn bị MS SQL Server , sau đó viết chuỗi kết nối vào file appsettings.json để sau này EF sử dụng.

Ứng dụng này ta chọn đặt tên CSDL trong MS SQL Server là myblog, nên cấu hình chuỗi kết nối có thể là:

 {
   "Logging": {
     "LogLevel": {
       "Default": "Information",
       "Microsoft": "Warning",
       "Microsoft.Hosting.Lifetime": "Information"
     }
   },
   "AllowedHosts": "*",  
 
   "ConnectionStrings": {
     "MyBlogContext": "Data Source=localhost,1433; Initial Catalog=myblog; User ID=SA;Password=Password123"
   }
 
     
 }
 

Lúc này bạn có thể tạo ra các Model, DbContext sau đó đăng ký vào hệ thống theo kiến thức các bài đã trình bày như: Tạo DbContext trong EF tạo DbContext EF trong Razor Page ...

Tuy nhiên ta sẽ không tạo database context kế thừa trực tiếp lớp DbContext mà sẽ kế thừa từ lớp phái sinh từ DbContext là IdentityDbContext - để tích hợp vào ứng dụng Identity, thư viện về xác thực trong ASP.NET

Tích hợp Identity vào ASP.NET MVC

Việc tích hợp Identity vào MVC thực hiện hoàn toàn giống với các bài viết về Identity trong Razor Page, bắt đầu từ bài Sử dụng Identity trong Razor Page

Hãy cài đặt các gói cần thiết

 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
 
 dotnet add package MailKit
 dotnet add package MimeKit
 

Tạo model AppUser

Lớp này kế thừa IdentityUser, nó có các trường định nghĩa sẵn (xem Model Identity ), định nghĩa thêm FullName, Address, Birthday

 using System;
 using System.ComponentModel.DataAnnotations;
 using Microsoft.AspNetCore.Identity;
 
 namespace mvcblog.Models {
 
     public class AppUser : IdentityUser {
 
         [MaxLength (100)]
         public string FullName { set; get; }
 
         [MaxLength (255)]
         public string Address { set; get; }
 
         [DataType (DataType.Date)]
         public DateTime? Birthday { set; get; }
 
     }
 }
 

Tạo model AppDbContext

Tạo một lớp kế thừa từ IdentityDbContext (có sẵn các bảng về Identity), ánh xạ nội dụng với CSDL (xem tạo AppDbContext )

Nội dung AppDbContext ở thời điểm này có thể như sau:

 using Microsoft.EntityFrameworkCore;
 using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
 using mvcblog.Models;
 
 namespace mvcblog.Data {
         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
             foreach (var entityType in builder.Model.GetEntityTypes ()) {
                 var tableName = entityType.GetTableName ();
                 if (tableName.StartsWith ("AspNet")) {
                     entityType.SetTableName (tableName.Substring (6));
                 }
             }
         }
 
     }
 
 }

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

Cập nhật Startup.ConfigureServices

 // Đăng ký AppDbContext, sử dụng kết nối đến MS SQL Server
 services.AddDbContext<AppDbContext> (options => {
     string connectstring = Configuration.GetConnectionString ("MyBlogContext");
     options.UseSqlServer (connectstring);
 });
 // Đăng ký các dịch vụ của Identity
 services.AddIdentity<AppUser, IdentityRole> ()
     .AddEntityFrameworkStores<AppDbContext> ()
     .AddDefaultTokenProviders ();
 
 // 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
 
 });
 
 // Cấu hình Cookie
 services.ConfigureApplicationCookie (options => {
     // options.Cookie.HttpOnly = true;  
     options.ExpireTimeSpan = TimeSpan.FromMinutes(30);  
     options.LoginPath = $"/login/";                                 // Url đến trang đăng nhập
     options.LogoutPath = $"/logout/";   
     options.AccessDeniedPath = $"/Identity/Account/AccessDenied";   // Trang khi User bị cấm truy cập
 });
 services.Configure<SecurityStampValidatorOptions>(options =>
 {
     // Trên 5 giây truy cập lại sẽ nạp lại thông tin User (Role)
     // SecurityStamp trong bảng User đổi -> nạp lại thông tinn Security
     options.ValidationInterval = TimeSpan.FromSeconds(5); 
 });
 

Thêm vào pipeline của ứng dụng, thêm tại Startup.configure

 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
 

Phát sinh cơ sở dữ liệu từ AddDbContext

Ở đây dùng kỹ thuật migration trong EntityFramework

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

 dotnet ef migrations add Init
 dotnet ef database update
 

Kết quả đã sinh ra CSDL ban đầu với cấu trúc:

Đến đây đã có thư viện Identity và CSDL đầy đủ, tuy nhiên ta sẽ không sử dụng code giao diện mặc định của Identity để có thể tùy biến. Bạn có thể thực hiện từ đầu theo các hướng dẫn bắt đầu tại: Phát sinh code (scaffold) Identity

Sử dụng lại mã nguồn Identity từ ví dụ trước

Để nhanh chóng có tất cả các trang chức năng đăng nhập, đăng ký, quyên mật khẩu ... , ta sẽ lấy lại toàn bộ kết quả của các ví dụ cũ về Identity của Razor Page đưa vào MVC, copy toàn bộ thư mục /Areas tại Album/Areas vào thư mục Areas của ứng dụng này.

Ở code cũ lớp AppUser ở namespace Album.Models và lớp AppDbContext ở namespace Album.Data trong khi ở ví dụ này thì hai lớp này định nghĩa lại ở namespace mvcblog.Data và mvcblog.Models nên hãy dùng tính năng tìm kiếm và thay thế để thay toàn bộ Album.ModelsAlbum.Data thành mvcblog.Modelsmvcblog.Data

Đồng thời cũng thay thế /Pages/Shared/_Layout.cshtml bằng /Views/Shared/_Layout.cshtml

Tiếp theo copy mã nguồn ViewComponent - MessagePage (để đúng cấu cấu thư mục) để tạo thông báo khi chuyển hướng, mã nguồn này đã xây dựng tại Tạo ViewComponent - MessagePage thông báo khi chuyển hướng

Tạm thời comment lại dòng using Album.Binder; và [ModelBinder(BinderType=typeof(DayMonthYearBinder))] trong Areas/Identity/Pages/Account/Manage/Index.cshtml.cs

Trong file Areas/Admin/Pages/Role/_ViewStart.cshtml và Areas/Admin/Pages/Role/_ViewStart.cshtml sửa thành

 @{
     Layout = "/Views/Shared/_Layout.cshtml";
 }
 

Triển khai dịch vụ gửi mail với IEmailSender

Dịch vụ gửi mail để Identity gửi mail trong trường hợp khi đăng ký tài khoản, lấy lại mật khẩu ... Sử dụng IEmailSender dùng gmail để gửi làm theo hướng dẫn Triển khai IEmailSender

Giờ các chức năng của Identity đã hoạt động như đã từng thực hành trên Razor Page, đã có thể đăng ký, đăng nhập ...

Thêm _LoginPartial.cshtml

Xây dựng thêm Partial có tên _LoginPartial.cshtml để chèn vào menu chính các mục chọn đăng nhập, đăng ký ..., xây dựng file này như sau:

Views/Shared/_LoginPartial.cshtml

 @using Microsoft.AspNetCore.Identity
 @using mvcblog.Models
 @using Microsoft.AspNetCore.Mvc.ViewEngines
 
 @inject SignInManager<AppUser> SignInManager
 @inject UserManager<AppUser> UserManager
 @inject ICompositeViewEngine Engine
 
 
 <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">Xin chào @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">Đăng xuất</button>
         </form>
     </li>
     
     @if (Engine.FindView(ViewContext, "_AdminDropdownMenu", false).Success) {
         @await Html.PartialAsync("_AdminDropdownMenu")
     }
 }
 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>

Trong file này để chèn HTML các mục menu để thêm vào thanh menu chính, nó xuất hiện ở bên phải, lưu ý ở file này:

Đoạn code:

 @inject ICompositeViewEngine Engine

Để Inject đối tượng ICompositeViewEngine, để có thể kiểm tra một partial có tồn tại hay không trước khi render, trong file này có render _AdminDropdownMenu.cshtml

 @if (Engine.FindView(ViewContext, "_AdminDropdownMenu", false).Success) {
     @await Html.PartialAsync("_AdminDropdownMenu")
 }
 

_AdminDropdownMenu.cshtml tạo một Dropdown menu - chung cấp các mục chọn đến chức năng quản lý role trong hệ thống, menu này chỉ xuất hiện khi User thỏa mãn policyAdminDropdown

policy AdminDropdown được tạo như sau (trong Startup.ConfigureServices)

Xem thêm: chứng thực quyền theo chính sách policy

 services.AddAuthorization(options =>
 {
     // User thỏa mãn policy khi có roleclaim: permission với giá trị manage.user
     options.AddPolicy("AdminDropdown", policy => {
         policy.RequireClaim("permission", "manage.user");
     });
 
 });
 

Nội dung _AdminDropdownMenu.cshtml như sau:

 @using Microsoft.AspNetCore.Identity
 @using mvcblog.Models
 @using Microsoft.AspNetCore.Authorization
 @inject SignInManager<AppUser> SignInManager
 
 @inject Microsoft.AspNetCore.Authorization.IAuthorizationService authorizationService
 @if (SignInManager.IsSignedIn(User) 
     && (await authorizationService.AuthorizeAsync(User, "AdminDropdown")).Succeeded)
 {
     <li class="nav-item dropdown">
         <a class="nav-item nav-link dropdown-toggle mr-md-2" href="#"  data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
             Manager
         </a>
         <div class="dropdown-menu dropdown-menu-right" aria-labelledby="bd-versions">
             <a class="dropdown-item" asp-page="/Role/Index" asp-area="Admin">Quản lý role</a>
 
             <div class="dropdown-divider"></div>
             <a class="dropdown-item" asp-page="/Role/User" asp-area="Admin">Gán role cho User</a>
         </div>
     </li>
 }

Trong Views/Shared/_Layout.cshtml thêm vào nội dung để chèn partial _LoginPartial.cshtml@await Html.PartialAsync("_LoginPartial"), vị trí chèn như sau:

 <!DOCTYPE html>
 /..
 <body>
     <header>
         <nav class="navbar ... ">
             <div class="container">
                 / ...
                 @await Html.PartialAsync("_LoginPartial")
             </div>
         </nav>
     </header>
     /...
 </body>
 </html>

Thêm chức năng tạo HTML Paging

Để có chức năng phân trang (ví dụ liệt kê 10 User / 1 trang) thì làm theo hướng dẫn Tạo partial phân trang HTML BootStrap trong ASP.NET truy vấn phân trang LINQ , tạo file Views/Shared/_Paging.cshtml và copy mã nguồn ở link trên vào

Tích hợp multiple-select

Trong mã nguồn có một số chỗ sử dụng thư viện JS multiple-select (như trong file Areas/Admin/Pages/Role/AddUserRole.cshtml), để phần tử HTML Select ở dạng dễ chọn hơn

File nào tích hợp thường có đoạn mã nạp và sử dụng thư viện

 <script src="~/lib/multiple-select/multiple-select.min.js"></script>
 <link rel="stylesheet" href="~/lib/multiple-select/multiple-select.min.css" />
 <script>
       $('#selectrole').multipleSelect({
             selectAll: false,
             keepOpen: false,
             isOpen: false
         });
 </script>

Nên hãy đảm bảo có 2 file thư viện là: wwwroot/lib/multiple-select/multiple-select.min.css vả wwwroot/lib/multiple-select/multiple-select.min.js

Để kiểm tra, cần đăng ký User, đăng nhập - tạo role cho user (https://localhost:5001/admin/role/), role này có roleclaim với tên và giá trị: permission: manage.user

Mã nguồn tham khảo ASP_NET_CORE/mvcblog, hoặc tải về bản bài này ex068-identity

Mã nguồn Identity MVC

Nguồn tin: Xuanthulab