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">×</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" />
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
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/
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 Role và Xó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
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" /> }
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"); }
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.
Đế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