造輪子之自定義授權策略

2023-10-09 21:01:32

前面我們已經弄好了使用者角色這塊內容,接下來就是我們的授權策略。在asp.net core中提供了自定義的授權策略方案,我們可以按照需求自定義我們的許可權過濾。
這裡我的想法是,不需要在每個Controller或者Action打上AuthorizeAttribute,自動根據ControllerName和ActionName匹配授權。只需要在Controller基礎類別打上一個AuthorizeAttribute,其他Controller除了需要匿名存取的,使用統一的ControllerName和ActionName匹配授權方案。
話不多說,開整。

IPermissionChecker

首先我們需要一個PermissionChecker來作為檢查當前操作是否有許可權。很簡單,只需要傳入ControllerName和ActionName。至於實現,後續再寫。

namespace Wheel.Authorization
{
    public interface IPermissionChecker
    {
        Task<bool> Check(string controller, string action);
    }
}

PermissionAuthorizationHandler

接下來我們則需要實現一個PermissionAuthorizationHandler和PermissionAuthorizationRequirement,繼承AuthorizationHandler抽象泛型類。

using Microsoft.AspNetCore.Authorization;

namespace Wheel.Authorization
{
    public class PermissionAuthorizationRequirement : IAuthorizationRequirement
    {
        public PermissionAuthorizationRequirement()
        {
        }

    }
}
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Controllers;
using Wheel.DependencyInjection;

namespace Wheel.Authorization
{
    public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionAuthorizationRequirement>, ITransientDependency
    {
        private readonly IPermissionChecker _permissionChecker;

        public PermissionAuthorizationHandler(IPermissionChecker permissionChecker)
        {
            _permissionChecker = permissionChecker;
        }

        protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionAuthorizationRequirement requirement)
        {
            if (context.Resource is HttpContext httpContext)
            {
                var actionDescriptor = httpContext.GetEndpoint()?.Metadata.GetMetadata<ControllerActionDescriptor>();
                var controllerName = actionDescriptor?.ControllerName;
                var actionName = actionDescriptor?.ActionName;
                if (await _permissionChecker.Check(controllerName, actionName))
                {
                    context.Succeed(requirement);
                }
            }
        }
    }
}

在PermissionAuthorizationHandler中注入IPermissionChecker。
然後通過重寫HandleRequirementAsync進行授權策略的校驗。
這裡使用HttpContext獲取請求的ControllerName和ActionName,再使用IPermissionChecker進行檢查,如果通過則放行,不通過則自動走AspNetCore的其他AuthorizationHandler流程,不需要呼叫context.Fail方法。

PermissionAuthorizationPolicyProvider

這裡除了AuthorizationHandler,還需要實現一個PermissionAuthorizationPolicyProvider,用於在匹配到我們自定義Permission的時候,就使用PermissionAuthorizationHandler做授權校驗,否則不會生效。

using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using Wheel.DependencyInjection;

namespace Wheel.Authorization
{
    public class PermissionAuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider, ITransientDependency
    {
        public PermissionAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options) : base(options)
        {
        }
        public override async Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
        {
            var policy = await base.GetPolicyAsync(policyName);
            if (policy != null)
            {
                return policy;
            }
            if (policyName == "Permission")
            {
                var policyBuilder = new AuthorizationPolicyBuilder(Array.Empty<string>());
                policyBuilder.AddRequirements(new PermissionAuthorizationRequirement());
                return policyBuilder.Build();
            }
            return null;
        }
    }
}

很簡單,只需要匹配到policyName == "Permission"時,新增一個PermissionAuthorizationRequirement即可。

PermissionChecker

接下來我們來實現IPermissionChecker的介面。

namespace Wheel.Permission
{
    public class PermissionChecker : IPermissionChecker, ITransientDependency
    {
        private readonly ICurrentUser _currentUser;
        private readonly IDistributedCache _distributedCache;

        public PermissionChecker(ICurrentUser currentUser, IDistributedCache distributedCache)
        {
            _currentUser = currentUser;
            _distributedCache = distributedCache;
        }

        public async Task<bool> Check(string controller, string action)
        {
            if (_currentUser.IsInRoles("admin"))
                return true;
            foreach (var role in _currentUser.Roles)
            {
                var permissions = await _distributedCache.GetAsync<List<string>>($"Permission:R:{role}");
                if (permissions is null)
                    continue;
                if (permissions.Any(a => a == $"{controller}:{action}"))
                    return true;
            }
            return false;
        }
    }
}

通過當前請求使用者ICurrentUser以及分散式快取IDistributedCache做許可權判斷,避免頻繁查詢資料庫。
這裡ICurrentUser如何實現後續文章再寫。
很簡單,先判斷使用者角色是否是admin,如果是admin角色則預設所有許可權放行。否則根據快取中的角色許可權進行判斷。如果通過則放行,否則拒絕存取。

建立抽象Controller基礎類別

建立WheelControllerBase抽象基礎類別,新增[Authorize("Permission")]的特性頭部,約定其餘所有Controller都繼承這個控制器。

    [Authorize("Permission")]
    public abstract class WheelControllerBase : ControllerBase
    {
        
    }

接下來我們測試一個需要許可權的API。




通過DEBUG可以看到我們正常走了校驗並響應401。

就這樣我們完成了我們自定義的授權策略設定。

輪子倉庫地址https://github.com/Wheel-Framework/Wheel
歡迎進群催更。