C# cơ bản .NET Core

Bài này tiếp tục thực hành, phát triển trên dự án của ví dụ Album trước: Identity (5) - Trang quản lý tài khoản cá nhân trong Identity

Giới thiệu về IdentityRole, RoleManager

Identity cung cấp các cách để xác nhận một User có quyền gì như Role-based authorization (chứng thực theo role - vai trò), Policy-based authorization (chứng thực theo chính sách) ... Phần này tìm hiểu trường hợp thứ nhất Role-based authorization, xác định quyền của User theo Role được gán cho User.

Đối tượng lớp IdentityRole là thông tin nếu gán cho User thì nó cho biết vai trò của User đó là gì, User đó được phép thực hiện những tác vụ trong ứng dựng. Các IdentityRole do bạn tạo ra được lưu trong bảng Roles của CSDL.

Để quản lý các IdentityRole trong Identity cung cấp dịch vụ RoleManager<IdentityRole> bạn có thể Inject vào Razor Page, Controller để sử dụng

Một số phương thức thuộc tính trong RoleManager<IdentityRole>

Member Mô tả
Roles Thuộc tính kiểu IQueryable<IdentityRole> - để truy vấn lấy các IdentityRole, ví dụ lấy danh sách các IdentityRole
List<IdentityRole> roles  =  await _roleManager.Roles.ToListAsync();
CreateAsync Tạo mới IdentityRole (chèn vào Database)
await _roleManager.CreateAsync(identityRole);
DeleteAsync Xóa IdentityRole
await _roleManager.DeleteAsync(identityRole);
RoleExistsAsync Kiểm tra sự tồn tại của một IdentityRole theo tên của nó
await _roleManager.RoleExistsAsync(roleName);
FindByIdAsync Lấy Role theo ID của nó
FindByNameAsync Lấy Role theo tên của nó

Xây dựng các trang quản lý Role cơ bản

Phần này tạo ra các trang gồm: Liệt kê danh sách các Roles (đang có trong database), thêm mới, cập nhật - sửa đổi, xóa role.

Ta xây dựng các trang Razor Page này trong thư mục Areas/Admin/Pages/Role. Đầu tiên tạo ra các file chung sau:

Areas/Admin/Pages/Role/_ViewImports.cshtml

@using Album.Areas.Admin.Pages.Role
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

File này thiết lập chung cho các trang trong thư mục Areas/Admin/Pages/Role/ gồm cấu hình để sử dụng được Tag Helper và nạp namespace Album.Areas.Admin.Pages.Role

Areas/Admin/Pages/Role/_ViewStart.cshtml

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

File này để thêm đoạn code trong nó vào tất cả cac file trong thư mục Role, nó thực hiện thiết lập layout của các trang là /Pages/Shared/_Layout

Areas/Admin/Pages/Role/_StatusMessage.cshtml

@model string

@if (!String.IsNullOrEmpty(Model))
{
    var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success";
    <div class="alert alert-@statusMessageClass alert-dismissible" role="alert">
        <button type="button" class="close" data-dismiss="alert" 
                 aria-label="Close"><span aria-hidden="true">&times;</span></button>
        @Model
    </div>
}

File này để tạo HTML là một hộp thông báo, nó được chèn vào khi các trang sử dụng với cấu trúc code

<partial name="_StatusMessage" model="@thongbao" />
role

Trang hiện thị danh sách các Role

Các Role (IdentityRole) được lưu trong bảng Roles của CSDL, tạm thời chèn vào bảng một vài Role để thực hành kiểm tra. Có thể chạy SQL sau:

insert into Roles (ID, [Name]) VALUES('ID1', 'Role1')
insert into Roles (ID, [Name]) VALUES('ID2', 'Role2')
insert into Roles (ID, [Name]) VALUES('ID3', 'Role3')

Câu lệnh SQL trên chèn vào bảng 3 Role với tên Role1, Role2, Role3

role

Giờ ta sẽ xây dựng trang Index hiện thị danh sách các Role này. Tạo trang Razor Page như sau:

Areas/Admin/Pages/Role/Index.cshtml.cs

namespace Album.Areas.Admin.Pages.Role
{
    public class IndexModel : PageModel
    {
        private readonly RoleManager<IdentityRole> _roleManager;

        public IndexModel(RoleManager<IdentityRole> roleManager)
        {
            _roleManager = roleManager;
        }
        public List<IdentityRole> roles {set; get;}

        [TempData] // Sử dụng Session lưu thông báo
        public string StatusMessage { get; set; }

        public async Task<IActionResult> OnGet()
        {
            roles  =  await _roleManager.Roles.ToListAsync();
            return Page();
        }
    }
}

Trong code (Index.cshtml.cs) trên, đã Inject dịch vụ RoleManager vào PageModel thông qua phương thức khởi tạo, sau đó trong OnGet lấy danh sách các Role rồi chuyển để hiện thị trên View (Index.cshtml)

StatusMessage là thông báo sẽ hiện thị ở View, do nó có thiết lập thuộc tính [TempData] có nghĩa giá trị của nó được phục hồi từ Session (có thể do Url khác thiết lập chuyển hướng đến - ví dụ khi tạo một Role mới ở trang Add trang này thiết lập một thông báo trong thuộc tính có cùng tên StatusMessage cũng sử dụng [TempData], thì khi trang đó chuyển về Index thì StatusMessage trong Index được phục hồi giá trị do chính trang Add thiết lập)

Areas/Admin/Pages/Role/Index.cshtml

@page "/admin/role/"
@model IndexModel

<h3>Danh sách các role</h1>
<partial name="_StatusMessage" model="@Model.StatusMessage" />

<form method="POST" class="my-1 d-inline">
    <button class="btn btn-secondary" asp-page="./Add" 
            asp-page-handler="StartNewRole">Thêm mới Role</button>
</form>
<a class="btn btn-secondary" asp-page="./User">Gán role cho người dùng</a>

<table class="table">
  <tr>
    <th>Role ID</th>
    <th>Tên</th>
    <th>Tác vụ</th>
  </tr>
  @foreach (var role in @Model.roles)
  {
      <tr>
        <td>@role.Id</td>
        <td>@role.Name</td>
        <td>
            <form method="POST" class="d-inline">
              <button name="Input.ID" value="@role.Id" class="btn btn-success btn-sm" 
                      asp-page="./Add" asp-page-handler="StartUpdate">Cập nhật Role</button>
            </form>
            <form method="POST" class="d-inline">
              <button name="Input.ID" value="@role.Id" class="btn btn-success btn-sm" 
                      asp-page="./Delete">Xóa Role</button>
            </form>
        </td>
      </tr>
  }
</table>

Kết quả chạy thử với url /admin/role/

role

Trong trang trên có nút bấm Thêm Role mới, nó thực hiện gọi đến handler StartNewRole (OnPostStartNewRole) của trang Add để thực hiện thêm một Role mới (ta sẽ xây dựng trang Add ngay sau đây)

Mỗi Role có nút bấm Cập nhật RoleXóa Role nó gọi đến handler - trang tương ứng để thực hiện, các trang này ta cũng xây dựng ở phần sau.

Trang Add để thêm mới, cập nhật Role

Areas/Admin/Pages/Role/Add.cshtml.cs

namespace Album.Areas.Admin.Pages.Role {
    public class AddModel : PageModel {
        private readonly RoleManager<IdentityRole> _roleManager;

        public AddModel (RoleManager<IdentityRole> roleManager) {
            _roleManager = roleManager;
        }

        [TempData] // Sử dụng Session
        public string StatusMessage { get; set; }

        public class InputModel {
            public string ID { set; get; }

            [Required (ErrorMessage = "Phải nhập tên role")]
            [Display (Name = "Tên của Role")]
            [StringLength (100, ErrorMessage = "{0} dài {2} đến {1} ký tự.", MinimumLength = 3)]
            public string Name { set; get; }

        }

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

        [BindProperty]
        public bool IsUpdate { set; get; }

        // Không cho truy cập trang mặc định mà không có handler
        public IActionResult OnGet () => NotFound ("Không thấy");
        public IActionResult OnPost () => NotFound ("Không thấy");


        public IActionResult OnPostStartNewRole () {
            StatusMessage = "Hãy nhập thông tin để tạo role mới";
            IsUpdate = false;
            ModelState.Clear ();
            return Page ();
        }

        // Truy vấn lấy thông tin Role cần cập nhật
        public async Task<IActionResult> OnPostStartUpdate () {
            StatusMessage = null;
            IsUpdate = true;
            if (Input.ID == null) {
                StatusMessage = "Error: Không có thông tin về Role";
                return Page ();
            }
            var result = await _roleManager.FindByIdAsync (Input.ID);
            if (result != null) {
                Input.Name = result.Name;
                ViewData["Title"] = "Cập nhật role : " + Input.Name;
                ModelState.Clear ();
            } else {
                StatusMessage = "Error: Không có thông tin về Role ID = " + Input.ID;
            }

            return Page ();
        }

        // Cập nhật hoặc thêm mới tùy thuộc vào IsUpdate
        public async Task<IActionResult> OnPostAddOrUpdate () {

            if (!ModelState.IsValid) {
                StatusMessage = null;
                return Page ();
            }

            if (IsUpdate) {
                // CẬP NHẬT
                if (Input.ID == null) {
                    ModelState.Clear ();
                    StatusMessage = "Error: Không có thông tin về role";
                    return Page ();
                }
                var result = await _roleManager.FindByIdAsync (Input.ID);
                if (result != null) {
                    result.Name = Input.Name;
                    // Cập nhật tên Role
                    var roleUpdateRs = await _roleManager.UpdateAsync (result);
                    if (roleUpdateRs.Succeeded) {
                        StatusMessage = "Đã cập nhật role thành công";
                    } else {
                        StatusMessage = "Error: ";
                        foreach (var er in roleUpdateRs.Errors) {
                            StatusMessage += er.Description;
                        }
                    }
                } else {
                    StatusMessage = "Error: Không tìm thấy Role cập nhật";
                }

            } else {
                // TẠO MỚI
                var newRole = new IdentityRole (Input.Name);
                // Thực hiện tạo Role mới
                var rsNewRole = await _roleManager.CreateAsync (newRole);
                if (rsNewRole.Succeeded) {
                    StatusMessage = $"Đã tạo role mới thành công: {newRole.Name}";
                    return RedirectToPage("./Index");
                } else {
                    StatusMessage = "Error: ";
                    foreach (var er in rsNewRole.Errors) {
                        StatusMessage += er.Description;
                    }
                }
            }

            return Page ();

        }
    }
}

Areas/Admin/Pages/Role/Add.cshtml

@page "/admin/role/updaterole/{handler?}/"
@model Album.Areas.Admin.Pages.Role.AddModel
@{
    var btnText = Model.IsUpdate ? "Cập nhật" : "Tạo mới";
}


<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" model="@Model.StatusMessage" />

<div class="row">
    <div class="col-md-6">
        <form method="post">
            <div asp-validation-summary="All" class="text-danger"></div>
            <input type="hidden"  asp-for="Input.ID">
            <input type="hidden"  asp-for="IsUpdate">
            <div class="form-group">
                <label asp-for="Input.Name"></label>
                <input asp-for="Input.Name" class="form-control">
                <span asp-validation-for="Input.Name" class="text-danger"></span>
            </div>

            <button id="add-or-edit-role" type="submit" 
                asp-page-handler="AddOrUpdate" class="btn btn-primary">@btnText</button>
                
            <a class="btn btn-primary" asp-page="./Index">Danh sách</a>
        </form>
    </div>
</div>

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

Giờ đã có thể thực hiện nút bấm Thêm Role mới, Cập nhật Role

role

Trang xóa role

Trang này thực hiện khi bấm vào nút Xóa role, xây dựng trang Delete như sau:

Areas/Admin/Pages/Role/Delete.cshtml.cs

namespace Album.Areas.Admin.Pages.Role {
  public class DeleteModel : PageModel {
    private readonly RoleManager<IdentityRole> _roleManager;

    public DeleteModel (RoleManager<IdentityRole> roleManager) {
      _roleManager = roleManager;
    }

    public class InputModel {
      [Required]
      public string ID { set; get; }
      public string Name { set; get; }

    }

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

    [BindProperty]
    public bool isConfirmed { set; get; }

    [TempData] // Sử dụng Session
    public string StatusMessage { get; set; }

    public IActionResult OnGet () => NotFound ("Không thấy");

    public async Task<IActionResult> OnPost () {

      if (!ModelState.IsValid) {
        return NotFound ("Không xóa được");
      }

      var role = await _roleManager.FindByIdAsync (Input.ID);
      if (role == null) {
        return NotFound ("Không thấy role cần xóa");
      }

      ModelState.Clear ();

      if (isConfirmed) {
        //Xóa
        await _roleManager.DeleteAsync (role);
        StatusMessage = "Đã xóa " + role.Name;

        return RedirectToPage ("Index");
      } else {
        Input.Name = role.Name;
        isConfirmed = true;

      }

      return Page ();
    }
  }
}

Areas/Admin/Pages/Role/Delete.cshtml

@page
@model Album.Areas.Admin.Pages.Role.DeleteModel
@{
   ViewData["Title"] = "Xóa role";
}

<h4>@ViewData["Title"]</h4>
<div class="row">
    <div class="col-md-6">
        <p>Bạn có chăc chắn xóa Role <strong>@Model.Input.Name</strong> </p>
        <form method="post">
            <div asp-validation-summary="All" class="text-danger"></div>
            <input type="hidden"  asp-for="Input.ID">
            <input type="hidden"  asp-for="isConfirmed">
            <a class="btn btn-primary" asp-page="./Index">Danh sách</a>
            <button type="submit" asp-page="./Delete" class="btn btn-danger">Xóa</button>
        </form>
    </div>
</div>

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

Vây là đủ các chức năng để tạo mới, cập nhật, xóa role. Hãy xóa các role đã có và tạo mới ba role đặt tên là Admin, Editor, VipMember. Mục đích để chứng thực nếu User là Admin thì có đầy đủ các quyền, nếu Editor thì có quyền biên tập các bài viết ...

Tiếp theo xây dựng chức năng gán Role cho User, nút bấm Gán role cho người dùng

Mã nguồn tham khảo ASP_NET_CORE/Album

Xây dựng các trang quản gán Role cho User

Trang liệt kê các User

Trang này chạy khi bấm vào nút Gán role cho người dùng, trang liệt kê danh sách User và và role của User, cho phép bấm cập nhật Role cho User. Trang xây dựng tại Areas/Admin/Pages/Role/AddUserRole.cshtml.cs

Areas/Admin/Pages/Role/User.cshtml.cs

namespace Album.Areas.Admin.Pages.Role {
    public class UserModel : PageModel {
        const int USER_PER_PAGE = 10;
        private readonly RoleManager<IdentityRole> _roleManager;
        private readonly UserManager<AppUser> _userManager;

        public UserModel (RoleManager<IdentityRole> roleManager,
                          UserManager<AppUser> userManager) {
            _roleManager = roleManager;
            _userManager = userManager;
        }

        public class UserInList : AppUser {
            // Liệt kê các Role của User ví dụ: "Admin,Editor" ...
            public string listroles {set; get;}
        }

        public List<UserInList> users;
        public int totalPages {set; get;}

        [TempData] // Sử dụng Session
        public string StatusMessage { get; set; }

        [BindProperty(SupportsGet=true)]
        public int pageNumber {set;get;}

        public IActionResult OnPost() => NotFound("Cấm post");

        public async Task<IActionResult> OnGet() {

            var cuser = await _userManager.GetUserAsync(User);
            await _userManager.AddToRolesAsync(cuser, new string[] { "Editor"});
        
            if (pageNumber == 0) 
                pageNumber = 1; 

            var lusers  = (from u in _userManager.Users
                          orderby u.UserName
                          select new UserInList() { 
                              Id = u.Id, UserName = u.UserName,
                          });


            int totalUsers = await lusers.CountAsync();
        

            totalPages = (int)Math.Ceiling((double)totalUsers / USER_PER_PAGE);  

            users = await lusers.Skip(USER_PER_PAGE * (pageNumber - 1)).Take(USER_PER_PAGE).ToListAsync();
        
            // users.ForEach(async (user) => {
            //     var roles = await _userManager.GetRolesAsync(user);
            //     user.listroles = string.Join(",", roles.ToList());
            // });

            foreach (var user in users)
            {
                var roles = await _userManager.GetRolesAsync(user);
                user.listroles = string.Join(",", roles.ToList());
            }

            return Page();
        }
    }
}

Areas/Admin/Pages/Role/User.cshtml

@page "/admin/role/users/"
@model Album.Areas.Admin.Pages.Role.UserModel
@{
    ViewData["Title"] = "DANH SÁCH NGƯỜI DÙNG";
}


<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" model="@Model.StatusMessage" />

<table class="table table-striped">
    <tr>
        <th>UserName</th>
        <th>Roles</th>
        <th>Actions</th>
    </tr>
    @foreach (var user in @Model.users)
    {
        <tr>
            <td>@user.UserName</td>
            <td>@user.listroles</td>
            <td>
                <form method="POST" class="d-inline">
                <button name="Input.ID" value="@user.Id" class="btn btn-primary btn-sm" 
                        asp-page="./AddUserRole">Cập nhật Role</button>
                </form>
            </td>
        </tr>
        
    }
</table>

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

    Func<int?,string> generateUrl = (int? _pagenumber)  => {
        return Url.Page("./User", new {pageNumber = _pagenumber});
    };

    var datapaging = new {
        currentPage = Model.pageNumber,
        countPages  = Model.totalPages,
        generateUrl =  generateUrl
    };

}
<partial name="_Paging" model="@datapaging" />

Trong code trang User.cshtml(.cs) trên chú ý:

Có thiết lập lấy User theo từng trang, và sử dụng kỹ thuật phân trang bạn đọc thêm tại Tạo partial phân trang HTML BootStrap trong ASP.NET truy vấn phân trang LINQ để biết chi tiết sử dụng _Paging.cshtml

Để kiểm tra bạn đăng ký nhiều User hoặc chạy đoạn mã sau để thêm 100 User

for (int i = 0; i < 100; i++)
{
    await _userManager.CreateAsync(new AppUser {UserName = "user" + i, Email = "user" + i + "@gmail.com"}, "123");
}
role

Xây dựng trang chức năng cập nhật Role

Trang này để thiết lập role cho từng User: AddUserRole.cshtml, AddUserRole.cshtml.cs

Areas/Admin/Pages/Role/AddUserRole.cshtml.cs

namespace Album.Areas.Admin.Pages.Role {
  public class AddUserRole : PageModel {
    private readonly RoleManager<IdentityRole> _roleManager;
    private readonly UserManager<AppUser> _userManager;


    public AddUserRole (RoleManager<IdentityRole> roleManager,
                        UserManager<AppUser> userManager) {
        _roleManager = roleManager;
        _userManager = userManager;
    }
    
    public class InputModel {
      [Required]
      public string ID { set; get; }
      public string Name { set; get; }

      public string[] RoleNames  {set; get;}

    }

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

    [BindProperty]
    public bool isConfirmed { set; get; }

    [TempData] // Sử dụng Session
    public string StatusMessage { get; set; }

    public IActionResult OnGet () => NotFound ("Không thấy");

    public List<string> AllRoles {set; get;} = new List<string>();

    public async Task<IActionResult> OnPost () {

      
      var user = await _userManager.FindByIdAsync (Input.ID);
      if (user == null) {
        return NotFound ("Không thấy role cần xóa");
      }

      var roles    = await _userManager.GetRolesAsync(user);
      var allroles = await _roleManager.Roles.ToListAsync();

      allroles.ForEach((r) => {
          AllRoles.Add(r.Name);
      });

      if (!isConfirmed) {
        Input.RoleNames = roles.ToArray();
        isConfirmed = true;
        StatusMessage = "";
        ModelState.Clear();
      }
      else {
        // Update add and remove
        StatusMessage = "Vừa cập nhật";
        if (Input.RoleNames == null) Input.RoleNames = new string[] {};
        foreach (var rolename in Input.RoleNames)
        {
            if (roles.Contains(rolename)) continue;
            await _userManager.AddToRoleAsync(user, rolename);
        }
        foreach (var rolename in roles)
        {
            if (Input.RoleNames.Contains(rolename)) continue;
            await _userManager.RemoveFromRoleAsync(user, rolename);
        }

      }

      Input.Name = user.UserName;
      return Page ();
    }
  }
}

Areas/Admin/Pages/Role/AddUserRole.cshtml

@page
@model Album.Areas.Admin.Pages.Role.AddUserRole
@{
   ViewData["Title"] = "Cập nhật role cho User";
}

<h4>@ViewData["Title"]</h4>
<div class="row">
    <div class="col-md-6">
        <p>Chọn các role gán cho <strong>@Model.Input.Name</strong></p>
        <form method="post">
            <partial name="_StatusMessage" model="@Model.StatusMessage" />
            <div class="form-group">
                @Html.LabelFor(x => x.Input.RoleNames)
                @Html.ListBoxFor(x => x.Input.RoleNames,
                    new SelectList( Model.AllRoles ),
                    new {@class="w-100", id = "selectrole"})
            </div>

            <div asp-validation-summary="All" class="text-danger"></div>
            <input type="hidden"  asp-for="Input.ID">
            <input type="hidden"  asp-for="isConfirmed">
            <a class="btn btn-primary" asp-page="./User">Danh sách</a>
            <button type="submit" class="btn btn-danger">Cập nhật</button>
        </form>
    </div>
</div>

@section Scripts {
    <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>


    <partial name="_ValidationScriptsPartial" />
}

Trong đoạn mã trên chú ý: Có sử dụng Html.ListBoxFor để tạo phần tử HTML <select> cho phép lựa chọn nhiều giá trị. Các giá trị lựa chọn trong là tên các Role lưu trong Model.AllRoles

Đồng thời có sử dụng thư viện Multiple Select, để chuyển nhiều lựa chọn thành dạng chọn checkbox, để ý đoạn mã section Scripts ở cuối.

role

Đến đây đã xây dựng xong các chức năng quản lý Role, gán role vào User. Bài tiếp theo chúng ta bắt đầu sử dụng Role để xác nhận quyền trong ASP.NET

Tham khảo code ASP_NET_CORE/Album hoặc tải về phiên bản đến chức năng của bài này ex063-role


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