Asp.Net Core MVC – Bảo mật và định danh (Phần II)

0 Comments

Ở bài trước chúng ta đã cùng nhau giải quyết bài toán bảo mật. Trong bài này chúng ta sẽ cùng nhau phân quyền cho chúng.

Xác thực với vai trò:
Role (vai trò) là một cách tiếp cận phổ biến để xử lý phân quyền trong ứng dụng Web. Ví dụ: thông thường vai trò quản trị viên cung cấp cho người dùng quản trị nhiều quyền hơn người dùng bình thường.
Ở dự án của chúng ta, bạn sẽ thêm một trang quản lý người dùng mà chỉ quản trị viên mới có thể truy cập.

Thêm một trang quản trị người dùng:
Đầu tiên hãy khởi tạo một Controller:
Controllers/ManageUsersController.cs
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using AspNetCoreTodo.Models;
using Microsoft.EntityFrameworkCore;

namespace AspNetCoreTodo.Controllers
{
        [Authorize(Roles = “Administrator”)]
        public class ManageUsersController : Controller
        {
                private readonly UserManager<ApplicationUser>
                _userManager;

               public ManageUsersController(
                      UserManager<ApplicationUser> userManager)
              {
                     _userManager = userManager;
              }

              public async Task<IActionResult> Index()
              {
                      var admins = (await _userManager
                      .GetUsersInRoleAsync(“Administrator”))
                      .ToArray();

                      var everyone = await _userManager.Users
                      .ToArrayAsync();

                      var model = new ManageUsersViewModel
                     {
                              Administrators = admins,
                              Everyone = everyone
                     };

                     return View(model);
             }
       }
}

Đặt thuộc tính Role trên thuộc tính [Authorize] sẽ đảm bảo rằng người dùng phải đăng nhập và có vai trò quản trị viên để truy cập trang.
Bước tiếp theo hãy khởi tạo một view model:
Models/ManageUsersViewModel.cs
using System.Collections.Generic;

namespace AspNetCoreTodo.Models
{
         public class ManageUsersViewModel
         {
                public ApplicationUser[] Administrators { get; set; }

                public ApplicationUser[] Everyone { get; set;}
        }
}
Cuối cùng là khởi tạo một view cho hành động Index trong thư mục Views/ManageUsers :
Views/ManageUsers/Index.cshtml
@model ManageUsersViewModel

@{
           ViewData[“Title”] = “Manage users”;
}

<h2>@ViewData[“Title”]</h2>

<h3>Administrators</h3>

<table class=”table”>
         <thead>
                 <tr>
                          <td>Id</td>
                          <td>Email</td>
                </tr>
         </thead>

         @foreach (var user in Model.Administrators)
        {
                <tr>
                         <td>@user.Id</td>
                         <td>@user.Email</td>
               </tr>
        }
</table>

<h3>Everyone</h3>

<table class=”table”>
        <thead>
                 <tr>
                          <td>Id</td>
                          <td>Email</td>
                </tr>
        </thead>

        @foreach (var user in Model.Everyone)
        {
                <tr>
                        <td>@user.Id</td>
                        <td>@user.Email</td>
               </tr>
        }
</table>

Khởi chạy ứng dụng của bạn và truy cập đường dẫn /ManageUsers trong khi đăng nhập với tài khoản người dùng bình thường. Bạn sẽ nhìn thấy một từ chối:

Đó là vì người dùng không tự động gán vai trò Quản trị viên.
Khởi tạo một tài khoản quản trị viên:
Vì lý do bảo mật, không cai có thể đăng ký tài khoản quản trị viên mới. Trên thực tế, vai trò quản trị viên không tồn tại trong cơ sở dữ liệu.
Bạn có thể thêm vai trò quản trị viên với một tài khoản vào cơ sở dữ liệu vào lần đầu chạy ứng dụng.

Khởi tạo một lớp ở thư mục gốc của dự án có tên SeedData:
SeedData.cs
using System;
using System.Linq;
using System.Threading.Tasks;
using AspNetCoreTodo.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace AspNetCoreTodo
{
          public static class SeedData
          {
                  public static async Task InitializeAsync(
                  IServiceProvider services)
                  {
                         var roleManager = services
                        .GetRequiredService<RoleManager<IdentityRole>>();
                         await EnsureRolesAsync(roleManager);

                         var userManager = services
                         .GetRequiredService<UserManager<ApplicationUser>>();
                         await EnsureTestAdminAsync(userManager);
                  }
          }
}

Phương thức InitializeAsync() sử dụng một IserviceProvicder (bộ các dịch vụ được thiết lập trong phương thức startup.ConfigureService() ) để lấy RoleManager và UserManager từ Asp.Net Core Identity.
Thêm 2 phương thức dưới InitializeAsync(). Đầu tiên là phương thức EnsureRolesAsync():
private static async Task EnsureRolesAsync(
RoleManager<IdentityRole> roleManager)
{
          var alreadyExists = await roleManager
          .RoleExistsAsync(Constants.AdministratorRole);

           if (alreadyExists) return;

           await roleManager.CreateAsync(
           new IdentityRole(Constants.AdministratorRole));
}

Phương thức này kiểm tra xem vai trò quản trị viên có tồn tại trong cơ sở dữ liệu hay không. Nếu không, nó sẽ tạo ra một quản trị viên. Thay vì liên tục gõ chuỗi “Adminstrator” hãy tạo một lớp nhỏ gọi là Constants để giữ giá trị:
Constants.cs
namespace AspNetCoreTodo
{
         public static class Constants
         {
                 public const string AdministratorRole = “Administrator”;
         }
}
Tiếp theo viết phương thức EnsureTestAdminAsync():
SeedData.cs
private static async Task EnsureTestAdminAsync(
UserManager<ApplicationUser> userManager)
{
         var testAdmin = await userManager.Users
         .Where(x => x.UserName == “admin@todo.local”)
         .SingleOrDefaultAsync();

          if (testAdmin != null) return;

          testAdmin = new ApplicationUser
         {
                 UserName = “admin@todo.local”,
                 Email = “admin@todo.local”
         };
         await userManager.CreateAsync(
         testAdmin, “NotSecure123!!”);
         await userManager.AddToRoleAsync(
         testAdmin, Constants.AdministratorRole);
}

Nếu chưa có người dùng có tên admin@todo.local trong cơ sở dũ liệu, phương thức này sẽ tạo một người dùng và gán mật khẩu tạm thời. Sau khi bạn đăng nhập lần đầu tiên, bạn nên thay đổi mật khẩu để an toàn hơn.

Tiếp theo bạn cần nói cho ứng dụng của bạn chạy logic này khi nó khởi động. Sửa đổi program.cs và cập nhật phương thức Main() để gọi một phương thức mới, InitializeDatabase():
Program.cs
public static void Main(string[] args)
{
        var host = BuildWebHost(args);
        InitializeDatabase(host);
        host.Run();
}

Sau đó thêm phương thức mới xuống dưới phương thức Main():
private static void InitializeDatabase(IWebHost host)
{
           using (var scope = host.Services.CreateScope())
           {
                  var services = scope.ServiceProvider;

                 try
                 {
                           SeedData.InitializeAsync(services).Wait();
                  }
                 catch (Exception ex)
                 {
                          var logger = services
                          .GetRequiredService<ILogger<Program>>();
                          logger.LogError(ex, “Error occurred seeding the DB.”);
                  }
          }
}

Thêm câu lệnh khai báo using lên đầu tệp:
using Microsoft.Extensions.DependencyInjection;

Phương thức này nhận bộ dịch vụ mà SeedData.InitializeAsync() cần. Khi bạn khởi động ứng dụng, tài khoản admin@todo.colcal sẽ được tạo và gán vai trò quản trị viên. Hãy thử đăng nhập bằng tài khoản này và điều hướng tới http://localhost:5000/ManageUsers. Bạn sẽ nhìn thấy một danh sách các tài khoản đã đăng ký với ứng dụng.

Kiểm tra xác ủy quyền trên một view:
Thuộc tính [Authorize] giúp dễ dàng thực hiện kiểm tra ủy quyền trong controller hoặc phương thức hành động, nhưng nếu bạn muốn kiểm tra trong view thì sao? Ví dụ: hiển thị liên kết Adminstrator trong thanh điều hướng nếu người dùng đăng nhập là quản trị viên.


Bạn có thể đưa UserManager trực tiếp vào view để thực hiện các loại kiểm tra phân quyền này. Để giữ cho view luôn gọn gàng và có tổ chức, hãy tạo một partial view mới để thêm một mục mới vào thanh điều hướng trong layout:
Views/Shared/_AdminActionsPartial.cshtml
@using Microsoft.AspNetCore.Identity
@using AspNetCoreTodo.Models

@inject SignInManager<ApplicationUser> signInManager
@inject UserManager<ApplicationUser> userManager

@if (signInManager.IsSignedIn(User))
{
           var currentUser = await userManager.GetUserAsync(User);

          var isAdmin = currentUser != null
          && await userManager.IsInRoleAsync(
          currentUser,
          Constants.AdministratorRole);

          if (isAdmin)
          {
                     <ul class=”nav navbar-nav navbar-right”>
                            <li>
                                     <a asp-controller=”ManageUsers”
                                     asp-action=”Index”>
                                                  Manage Users
                                     </a>
                          </li>
                   </ul>
           }
}

Partial View này trước tiên sử dụng SignInManager để nhanh chóng xác định xem người dùng có đăng nhập hay không. Nếu không phần còn lại của view có thể bỏ qua. Nếu có người dùng đăng nhập, UserManager được sử dụng để tra cứu chi tiết người dùng và thực hiện kiểm tra ủy quyền với IsInRoleAsync(). Nếu tất cả thành công và người dùng là người quản trị, liên kết Manage users sẽ được thêm vào thanh điều hướng.

Để sư dụng Partial View trong layout, thêm đoạn mã sau:
Views/Shared/_Layout.cshtml
<div class=”navbar-collapse collapse”>
       <ul class=”nav navbar-nav”>
                <!– existing code here –>
       </ul>
       @await Html.PartialAsync(“_LoginPartial”)
       @await Html.PartialAsync(“_AdminActionsPartial”)
</div>
Khi bạn đăng nhập với tài khoản quản trị viên, bạn sẽ nhìn thấy một mục mới:

More Resources:
Asp.Net Core Identity giúp bạn thêm các tính năng bảo mật và nhận dạng như đăng nhập và đăng ký vào ứng dụng của bạn. Các mẫu của dotnet new cung cấp cho bạn các view và các controller được xây dựng sẵn để xử lý các tính năng này.

Asp.Net Core Identity có thể làm nhiều hơn vậy, chẳng hạn như đặt lại mật khẩu và đăng nhập dựa trên các ứng dụng khác. Hãy tham khảo chúng tại http://docs.asp.net

Security concept: pixelated Locks icon on digital background, 3d render

Các lựa chọn thay thế cho Asp.Net Core Identity:
Asp.Net Core Identity không phải là cách duy nhất để thêm chức năng định danh. Một cách tiếp cận khác là sử dụng nhận dạng được lưu trữ trên đám mây như Azure Active Directory B2C hoặc Okta để xử lý danh tính cho ứng dụng của bạn. Bạn có thể nghĩ về các tùy chọn này như là một phần của sự phát triển:
– Do-It-YourSelt security: Không được đề xuất trừ khi bạn là chuyên gia bảo mật.
– Asp.Net Core Identity: Bạn nhận được rất nhiều mã miễn phí với các mẫu điều này giúp việc bắt đầu khá dễ dàng. Bạn sẽ cần viết một số mã cho các kịch bản nâng cao hơn và duy trì cơ sở dữ liệu để lưu trữ thông tin người dùng.
– Cloud-hosted identity services: Dịch vụ xử lý cả các kịch bản đơn giản và nâng cao (xác thực đa yếu tố, phục hồi tài khoản, liên kết) và giảm đáng kể số lượng mã bạn cần viết trong ứng dụng. Ngoài ra dữ liệu người dùng nhạy cảm không được lưu trữ trong cơ sở dữ liệu riêng của bạn.
Đối với dự án này Asp.Net Core Identity là một sự lựa chọn phù hợp. Đối với các dự án phức tạp hơn, chúng ta nên thực hiện một số nghiên cứu và thử nghiệm với cả hai tùy chọn để hiểu cách nào là tốt nhất đối với dự án của bạn.

Categories:

Leave a Reply

Your email address will not be published. Required fields are marked *