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: Xây dựng Authorization Handler riêng xử lý các yêu cầu từ IAuthorizationRequirement

Sử dụng IAuthorizationService chứng thực quyền

Khi bạn sử dụng thuộc tính [Authorize] trong các Controller, Action, Razor Page để xác định quyền truy cập - thực chất đã sử dụng dịch vụ IAuthorizationService trong hệ thống để thực hiện các Authorization Handler.

Trong nhiều tình huống - ngay trong code bạn muốn thi hành tác vụ kiểm tra quyền thì bạn phải lấy được đối tượng dịch vụ IAuthorizationService. Để có đối tượng này bạn có thể Inject qua phương thức khởi tạo của Controller của PageModel, hay Inject trong View

Ví dụ, trang Razor Page và Controller sau được Inject dịch vụ IAuthorizationService

public class TestAuthorize1Model : PageModel
{
    private readonly IAuthorizationService _authorizationService;

    // Inject IAuthorizationService bằng phương thức khởi tạo
    public TestAuthorize1Model(IAuthorizationService authorizationService) {

        _authorizationService = authorizationService;
    }
    /...
}
public class HomeController : Controller
{
    private readonly IAuthorizationService _authorizationService;

    public HomeController(IAuthorizationService authorizationService) {
        _authorizationService = authorizationService;
    }

    public ActionResult Index()
    {
        return View();
    }
}

Đối với Controller cũng Inject một cách tương tự như vậy, trong các View (.cshtml) muốn Inject có thể dùng chỉ thị @enject

@inject Microsoft.AspNetCore.Authorization.IAuthorizationService authorizationService

Sau khi có đối tượng IAuthorizationService, gọi phương thức AuthorizeAsync để chứng thực. Ví dụ:

// Kiểm tra xem User có thỏa mãn chính sách policyname
var result = await _authorizationService.AuthorizeAsync(User, "policyname");

if (!result.Succeeded)
{
    // Không có quyền
} else {
    // Có quyền
}s

Các phiên bản theo tham số của AuthorizeAsync:

AuthorizeAsync(ClaimsPrincipal user, object resource, IAuthorizationRequirement requirements);

AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName);

AuthorizeAsync(ClaimsPrincipal user, AuthorizationPolicy policy);

AuthorizeAsync(ClaimsPrincipal user, object resource, AuthorizationPolicy policy);

AuthorizeAsync(ClaimsPrincipal user, object resource, IAuthorizationRequirement requirement);

AuthorizeAsync(ClaimsPrincipal user, string policyName);

Ví dụ Sử dụng IAuthorizationService chứng thực quyền

Ví dụ - xác định quyền User được phép cập nhật (sửa) một bài đăng Post khi User chính là người tạo bài đó hoặc có Role là Admin

Giả sử một bài Post là đối tượng thuộc lớp Post có cấu trúc đơn giản như sau:

Models/Post.cs

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Album.Models
{
    public class Post
    {
        [Key]
        public int ID;
        public string title {set; get;}
        public string content {set; get;}
        public DateTime publishedDate {set; get;}

        // UserID là ID của User đăng bài
        public string UserID {set; get;}
        [ForeignKey("UserID")]
        public AppUser User {set; get;}
    }
}

Để xác định quyền như trên (xem Xây dựng Authorization Handler ) trước tiên xây dựng một yêu cầu - IAuthorizationRequirement đặt tên là CanUpdatePost như sau:

public class CanUpdatePostRequirement : IAuthorizationRequirement
{
        public bool AdminCanUpdate {set;get;}
        public bool OwnerCanUpdate {set; get;}
        public CanUpdatePostRequirement(bool _adminCanUpdate = true,  bool _ownerCanupdate = true)
        {
            AdminCanUpdate = _adminCanUpdate;
            OwnerCanUpdate = _ownerCanupdate;
        }
}

Xây dựng Handler xử lý CanUpdatePostRequirement và thông tin bài Post gửi đến, đặt tên Handler này là:

public class CanUpdatePostAgeHandler : AuthorizationHandler<CanUpdatePostRequirement, Post> {

    private readonly ILogger<MinimumAgeHandler> _logger;
    private readonly UserManager<AppUser> _userManager;

    public CanUpdatePostAgeHandler (ILogger<MinimumAgeHandler> logger,
        UserManager<AppUser> userManager) {
        _logger = logger;
        _userManager = userManager;
    }
    protected Task HandleRequirementAsync (AuthorizationHandlerContext context, 
                                           MinimumAgeRequirement requirement) {

        var user = _userManager.GetUserAsync (context.User).Result;
        if (user == null)
            return Task.CompletedTask;

        var dateOfBirth = user.Birthday;
        if (dateOfBirth == null) {
            _logger.LogInformation ("Không có ngày sinh");
            // Trả về mà chưa chứng thực thành công
            return Task.CompletedTask;
        }

        int calculatedAge = DateTime.Today.Year - dateOfBirth.Value.Year;
        if (dateOfBirth > DateTime.Today.AddYears (-calculatedAge)) {
            calculatedAge--;
        }

        if (calculatedAge < requirement.MinimumAge) {
            _logger.LogInformation (calculatedAge + ": Không đủ tuổi truy cập");
            return Task.CompletedTask;
        }

        // https://stackoverflow.com/a/12998855/4776710
        TimeSpan start = new TimeSpan (requirement.OpenTime, 0, 0);
        TimeSpan end = new TimeSpan (requirement.CloseTime, 0, 0);
        TimeSpan now = DateTime.Now.TimeOfDay;
        // see if start comes before end
        if (start < end)
            if (!(start <= now && now <= end)) {
                _logger.LogInformation (now.ToString () + " Không trong khung giờ được truy cập");
                return Task.CompletedTask;
            }
        // start is after end, so do the inverse comparison
        if (end < now && now < start) {
            _logger.LogInformation (now.ToString () + " Không trong khung giờ được truy cập");
            return Task.CompletedTask;
        }

        // Thiết lập chứng thực thành công
        context.Succeed (requirement);
        return Task.CompletedTask;
    }

    protected override Task HandleRequirementAsync (AuthorizationHandlerContext context, CanUpdatePostRequirement requirement, Post resource) {
        // Nếu cho Admin cập nhật thì kiểm tra User có RoleClaim Admin
        if (requirement.AdminCanUpdate) {
            if (context.User.IsInRole ("Admin")) {
                // Cho phép
                _logger.LogInformation ("Admin được cập nhật");
                context.Succeed (requirement);
                return Task.CompletedTask;
            }
        }

        if (context.User.Identity?.IsAuthenticated != true) {
            _logger.LogInformation ("User không đăng nhập");
            return Task.CompletedTask;
        }

        if (requirement.OwnerCanUpdate) {
            var user =  _userManager.GetUserAsync(context.User).Result;
            if (user.Id == resource.UserID) 
            {
                _logger.LogInformation ("Được phép cập nhật");
                context.Succeed (requirement);
                return Task.CompletedTask;
            }
        }

        _logger.LogInformation ("Không được phép cập nhật");
        return Task.CompletedTask;
    }
}

Trong lớp Handler trên khai báo kế thừa từ CanUpdatePostAgeHandler : AuthorizationHandler<CanUpdatePostRequirement, Post>, khác với ví dụ trước - ngoài Requriemennt còn có lớp sẽ là tài nguyên truyề đến khi chứng thực. Và phải ghi đè phương thức là:

protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                               CanUpdatePostRequirement requirement, Post resource)

Đăng ký Handler vào dịch vụ hệ thống

services.AddTransient<IAuthorizationHandler, CanUpdatePostAgeHandler>();

Kiểm tra

public class TestAuthorize1Model : PageModel
{

    private readonly IAuthorizationService _authorizationService;
    public TestAuthorize1Model(IAuthorizationService authorizationService) {

        _authorizationService = authorizationService;
    }

    public async Task<IActionResult> OnGet()
    {
        // Post thực tế được nạp từ DB ... ở đây để kiểm tra tạo đối tượng
        // như sau
        var post = new Post() {
            UserID = "51d9d99d-85fe-411e-b36e-1bf981cb9db3", // thay bằng các ID khác nhau để kiểm tra
        };

        // Kiểm tra nhóm Admin hoặc chủ sở hữu Post thì có quyênn
        var rs = await _authorizationService.AuthorizeAsync(User, post,
                                                            new CanUpdatePostRequirement(true, true));
        if (!rs.Succeeded) {
            return Forbid();
        }
        // Có quyền
        return Page();
    }
}

Ví dụ 2, tạo một partial - chèn vào một Dropdown Menu theo quyền User

Dropdown menu được chèn vào Navbar (thanh menu trên) nếu User có RoleClaim (vai trò) permission với giá trị manage.user

Trước tiên tạo một policy đặt tên là AdminDropdown

options.AddPolicy("AdminDropdown", policy => {
    policy.RequireClaim("permission", "manage.user");
});

Tạo một partial Pages/Shared/_AdminDropdownMenu.cshtml

@using Microsoft.AspNetCore.Identity
@using Album.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>
}

Đoạn mã kiểm tra theo policy

(await authorizationService.AuthorizeAsync(User, "AdminDropdown")).Succeeded

Có thể chèn partial trên vào Pages/Shared/_LoginPartial.cshtml

<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>
    //...
    
    @await Html.PartialAsync("_AdminDropdownMenu.cshtml")

}
else
{
    //...
}
</ul>

Tham khảo code ASP_NET_CORE/Album


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