.net core 自定義授權策略提供程式進行許可權驗證

2023-03-23 18:02:15

.net core 自定義授權策略提供程式進行許可權驗證

在這之前先了解一下鑑權和授權的概念;

鑑權

鑑權可以說是身份驗證,身份驗證是確定使用者身份的過程;

在ASP.NET Core 中身份驗證是由身份驗證服務IAuthenticationService負責的,它被身份驗證中介軟體使用, 身份驗證服務會使用已註冊的身份驗證處理程式來完成與身份驗證相關的操作。身份驗證相關的操作包括:對使用者身份進行驗證,對未經身份驗證的使用者進行資源存取時做出響應。

身份驗證處理程式及其設定選項

身份驗證處理程式包括CookieAuthenticationHandler 和 JwtBearerHandler,身份驗證處理程式的註冊 是在呼叫AddAuthentication之後擴充套件方法AddJwtBearer 和 AddCookie 提供的

身份驗證處理程式會由實現IAuthenticationService 介面的AuthenticationService 的AuthenticateAsync 方法去呼叫

授權

授權是確定使用者是否有權存取資源的過程,這裡先簡單帶過一下後面接著講

授權方案

授權方案包括 基於角色的授權,基於宣告的授權,基於策略的授權,這裡著重說一下策略授權,

基於策略的授權

授權策略包含一個或多個要求

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AtLeast21", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(21)));
});

在前面的範例中,建立了「AtLeast21」策略。 該策略有一個最低年齡要求,其作為要求的引數提供

IAuthorizationRequirement

IAuthorizationRequirement用於跟蹤授權是否成功的機制

在IAuthorizationHandler 的 HandleAsync方法中 作為引數被呼叫,由HttpContext 的Requirements 屬性提供

IAuthorizationHandler

IAuthorizationHandler 用於檢查策略是否滿足要求,主要執行的方法是HandleAsync,我們可以繼承微軟提供的AuthorizationHandler,預設實現了HandlAsync ,在具有多個IAuthorizationRequirement 的情況下預設是迴圈去執行HandleRequirementAsync方法,在某些情況下我們可以去重寫從而去執行特定IAuthorizationRequirement,當然方法多樣

        public virtual async Task HandleAsync(AuthorizationHandlerContext context)
        {
            if (context.Resource is TResource)
            {
                foreach (var req in context.Requirements.OfType<TRequirement>())
                {
                    await HandleRequirementAsync(context, req, (TResource)context.Resource);
                }
            }
        }
IAuthorizationPolicyProvider

IAuthorizationPolicyProvider 自定義策略提供程式

繼承IAuthorizationPolicyProvider 需要去實現IAuthorizationPolicyProvider 三個方法,三個方法的執行由我們我們的Authorize特性決定,並且Authorize特性的策略名稱會傳遞到 IAuthorizationPolicyProvider 的GetPolicyAsync 作為引數使用

        public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
        {
            var policy = new AuthorizationPolicyBuilder();
            //新增鑑權方案
            policy.AddAuthenticationSchemes("Bearer");
            policy.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
            if (policyName is null)
            {
                return Task.FromResult<AuthorizationPolicy>(null);
            }
            var authorizations = policyName.Split(',');
            if (authorizations.Any())
            {
                //許可權策略構建器,新增自定義的AuthorizaRequirement
                policy.AddRequirements(new AuthorizeRequirement(authorizations));
            }
            return Task.FromResult(policy.Build());
        }

這裡看一下Authorize特性相關的屬性

    public class AuthorizeAttribute : Attribute, IAuthorizeData
    {
        /// <summary>
        /// </summary>
        public AuthorizeAttribute() { }

        /// <summary>
        /// 初始化類類範例並且設定可存取資源策略名稱
        /// </summary>
        /// <param name="policy">The name of the policy to require for authorization.</param>
        public AuthorizeAttribute(string policy)
        {
            Policy = policy;
        }
        /// <summary>
        /// 設定或獲取可以存取資源策略名稱
        /// </summary>
        public string? Policy { get; set; }

        /// <summary>
        /// 設定或獲取可以存取資源的角色
        /// </summary>
        public string? Roles { get; set; }

        /// <summary>
        /// 設定或獲取可以存取資源鑑權方案名稱
        /// </summary>
        public string? AuthenticationSchemes { get; set; }
    }
GetDefaultPolicyAsync

預設策略,當我們的Authorize特性上不提供策略時執行,這裡的CustomAuthorization特性是繼承了Authorize特性

        [CustomAuthorization]
        [HttpGet("SetNotOP")]
        [NoResult]
        public int SetNotOP()
        {
            throw new ArgumentNullException(nameof(TestTask));
            return 1;
        }
GetPolicyAsync

在Authorize新增策略時執行

        [CustomAuthorization("test1", "test1")]
        [HttpGet("TestTask")]
        public async Task<int> TestTask()
        {
            await Task.CompletedTask;
            return 1;
        }
GetFallbackPolicyAsync

後備授權策略,是指在沒有為請求指定其他策略時,由授權中介軟體提供的策略。在這裡可以指返回空值,也可以設定指定策略返回

    public Task<AuthorizationPolicy?> GetFallbackPolicyAsync()
    {
        return Task.FromResult<AuthorizationPolicy>(null);
    }
    //或者
    public Task<AuthorizationPolicy?> GetFallbackPolicyAsync()
    {
        var policy = new AuthorizationPolicyBuilder();
        policy.AddAuthenticationSchemes("Bearer");
        policy.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
        return Task.FromResult<AuthorizationPolicy>(policy.Build());
    }
IAuthorizationService

IAuthorizationService是確認授權成功與否的主要服務,兵器 負責去執行我們自定義的AuthorizationHandle

看一段由微軟官方簡化授權服務的程式碼,可以看到AuthorizeAsync會去迴圈執行自定義的AuthorizationHandle

public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, 
             object resource, IEnumerable<IAuthorizationRequirement> requirements)
{
    // Create a tracking context from the authorization inputs.
    var authContext = _contextFactory.CreateContext(requirements, user, resource);

    // By default this returns an IEnumerable<IAuthorizationHandlers> from DI.
    var handlers = await _handlers.GetHandlersAsync(authContext);

    // Invoke all handlers.
    foreach (var handler in handlers)
    {
        await handler.HandleAsync(authContext);
    }

    // Check the context, by default success is when all requirements have been met.
    return _evaluator.Evaluate(authContext);
}

到這裡應該對整個授權流程有了個大致的瞭解,在授權前會由鑑權中介軟體進行一個鑑權,鑑權通過後由IAuthorizationPolicyProvider 來提供一個授權策略(授權策略裡可以新增我們需要的IAuthorizationRequirement),最後由IAuthorizationService 的HandleAsync去執行自定義AuthorizeHandle

具體實現

自定義特性
    public class CustomAuthorizationAttribute : AuthorizeAttribute
    {
        public virtual string[] AuthorizeName { get; private set; }

        public CustomAuthorizationAttribute(params string[] authorizeName)
        {
            AuthorizeName = authorizeName;
            Policy = string.Join(",", AuthorizeName);
        }
    }
自定義策略提供程式
    public class AuthorizationProvider : IAuthorizationPolicyProvider
    {
        public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
        {
            var policy = new AuthorizationPolicyBuilder();
            policy.AddAuthenticationSchemes("Bearer");
            policy.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
            //預設授權測率必須新增一個IAuthorizationRequirement的實現
            policy.AddRequirements(new AuthorizeRequirement());
            return Task.FromResult<AuthorizationPolicy>(policy.Build());
        }

        public Task<AuthorizationPolicy?> GetFallbackPolicyAsync()
        {
            return Task.FromResult<AuthorizationPolicy>(null);
        }

        public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
        {
            var policy = new AuthorizationPolicyBuilder();
            policy.AddAuthenticationSchemes("Bearer");
            policy.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
            if (policyName is null)
            {
                return Task.FromResult<AuthorizationPolicy>(null);
            }
            var authorizations = policyName.Split(',');
            if (authorizations.Any())
            {
                policy.AddRequirements(new AuthorizeRequirement(authorizations));
            }
            return Task.FromResult(policy.Build());
        }
    }
自定義授權處理程式

IPermissionsCheck 是我注入的許可權檢測程式,其實對於許可權認證,重要的是控制對資源的存取,整篇文章下來無非就是將特性上的值提供到我們所需要進行許可權檢測的程式中去,當然我們也可以用許可權過濾器反射獲取Authorize特性上的值來實現

public class AuthorizeHandler : AuthorizationHandler<AuthorizeRequirement>
{
    private readonly IPermissionCheck _permisscheck;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public AuthorizeHandler(IHttpContextAccessor httpContextAccessor
        , IServiceProvider serviceProvider)
    {
        using var scope = serviceProvider.CreateScope();
        _permisscheck = scope.ServiceProvider.GetRequiredService<IPermissionCheck>();
        _httpContextAccessor = httpContextAccessor;
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthorizeRequirement requirement)
    {
        var identity = _httpContextAccessor?.HttpContext?.User?.Identity;
        var httpContext = _httpContextAccessor?.HttpContext;
        var isAuthenticated = identity?.IsAuthenticated ?? false;
        var claims = _httpContextAccessor?.HttpContext?.User?.Claims;
        var userId = claims?.FirstOrDefault(p => p.Type == "Id")?.Value;
        //判斷是否通過鑑權中介軟體--是否登入
        if (userId is null || !isAuthenticated)
        {
            context.Fail();
            return;
        }
        var defaultPolicy = requirement.AuthorizeName?.Any() ?? false;
        //預設授權策略
        if (!defaultPolicy)
        {
            context.Succeed(requirement);
            return;
        }
        var roleIds = claims?
            .Where(p => p?.Type?.Equals("RoleIds") ?? false)
            .Select(p => long.Parse(p.Value));
        var roleNames = claims?
            .Where(p => p?.Type?.Equals(ClaimTypes.Role) ?? false)
            .Select(p => p.Value);
        UserTokenModel tokenModel = new UserTokenModel()
        {
            UserId = long.Parse(userId ?? "0"),
            UserName = claims?.FirstOrDefault(p => p.Type == ClaimTypes.Name)?.Value ?? "",
            RoleNames = roleNames?.ToArray(),
            RoleIds = roleIds?.ToArray(),
        };
        if (requirement.AuthorizeName.Any())
        {
            if (!_permisscheck.IsGranted(tokenModel, requirement.AuthorizeName))
            {
                context.Fail();
                return;
            }
        }
        context.Succeed(requirement);
    }
}
自定義IAuthorizationRequirement
public class AuthorizeRequirement : IAuthorizationRequirement
{
    public virtual string[] AuthorizeName { get; private set; }
    public AuthorizeRequirement(params string[] authorizeName)
    {
        AuthorizeName = authorizeName;
    }

    public AuthorizeRequirement() { }
}
自定義授權結果中介軟體

自定義授權結果中介軟體的作用,返回自定義響應,增強預設質詢或禁止響應

    public class AuthorizeMiddleHandle : IAuthorizationMiddlewareResultHandler
    {
        public async Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy                                               policy, PolicyAuthorizationResult authorizeResult)
        {
            if (!authorizeResult.Succeeded || authorizeResult.Challenged)
            {
                var isLogin = context?.User?.Identity?.IsAuthenticated ?? false;
                var path = context?.Request?.Path ?? "";
                context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                var response = new AjaxResponse();
                response.UnAuthorizedRequest = true;
                response.StatusCode = "401";
                var error = new ErrorInfo();
                error.Error = isLogin ? $"你沒有許可權存取該介面-介面路由{path}" : "請先登入系統";
                response.Error = error;
                await context.Response.WriteAsJsonAsync(response);
                return;
            }
            await next(context);
        }
    }
相關服務的註冊
            context.Services.AddScoped<IPermissionCheck, PermissionCheck>();
            context.Services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationProvider>();
            context.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, AuthorizeMiddleHandle>();
            context.Services.AddSingleton<IAuthorizationHandler, AuthorizeHandler>();

到這裡對於許可權認證有了個大概的瞭解,至於是通過自定義策略提供程式自定義AuthorizHandle這一系列複雜的操作還是通過許可權過濾器取決看官自己。個人認為通過自定義策略提供程式自定義AuthorizHandle這種方式更靈活性,能夠應對更多複雜場景。