
对于一个 web 项目,Filter 是在构建构建 builder 的时候添加的
builder.Services.AddControllers(options =>{ options.Filters.Add<>()})这里不可能让用户手动添加,所以需要有一个扩展方法给用户调用
using Microsoft.AspNetCore.Mvc;namespace DotNetNB.Security.ActionAccess{ public static class MvcOptionsExtensions { public static MvcOptions AddActionAccessControl(this MvcOptions options) { options.Filters.Add<DynamicAuthorizeFilter>(); return options; } }}创建 Resource,包含 Key 和 Data 两个属性
namespace DotNetNB.Security.Core.Models{ public class Resource { public string Key { get; set; } public object Data { get; set; } }}创建 ActionResource,继承 Resource,包含 ControllerName,ActionName,RouteTemplate 和 HttpVerb 几个属性
using DotNetNB.Security.Core.Models;namespace DotNetNB.Security.ActionAccess{ public class ActionResource : Resource { } public class ActionResourceData { public string? ControllerName { get; set; } public string? ActionName { get; set; } public string DisplayName { get; set; } public string? RouteTemplate { get; set; } public string? HttpVerb { get; set; } }}定义一个 IResourceManager 接口,提供创建资源的方法
using DotNetNB.Security.Core.Models;namespace DotNetNB.Security.Core{ public interface IResourceManager { public Task CreateAsync(Resource resource); public Task CreateAsync(IEnumerable<Resource> resources); }}参考 ASP .NET Core 源码中的 ActionEndpointDataSourceBase:
https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Routing/ActionEndpointDataSourceBase.cs
创建 endpoint 的时候有一个 action 的列表 ActionDescriptors
var endpoints = CreateEndpoints(_actions.ActionDescriptors.Items, Conventions);它的类型是一个 IActionDescriptorCollectionProvider,专门用于扫描获取所有的 action
private readonly IActionDescriptorCollectionProvider _actions;在 ActionAccess 模块的 ActionResourceProvider 中把它加进来
using Microsoft.AspNetCore.Mvc.Infrastructure;namespace DotNetNB.Security.ActionAccess{ public class ActionResourceProvider { private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider; public ActionResourceProvider(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider) { _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; } }}在 host 启动的时候扫描获取所有的 action 信息,定义一个 IResourceProvider 接口
namespace DotNetNB.Security.Core{ public interface IResourceProvider { public Task<IEnumerable<Resource>> ExecuteAsync(); }}ActionResourceProvider 实现这个接口,从 ActionDescriptors 获取 action 信息,构建 ActionResourceData
public async Task<IEnumerable<Resource>> ExecuteAsync(){ var actions = _actionDescriptorCollectionProvider.ActionDescriptors.Items; var actionResources = new List<ActionResource>(); foreach (var action in actions) { if (action is ControllerActionDescriptor) { var actionDescriptor = action as ControllerActionDescriptor; var httpMethod = action.EndpointMetadata.Where(m => m is HttpMethodMetadata).FirstOrDefault() as HttpMethodMetadata; var routeAttribute = actionDescriptor?.EndpointMetadata.FirstOrDefault(m => m is RouteAttribute) as RouteAttribute; var resourceData = new ActionResourceData(); resourceData.HttpVerb = httpMethod?.HttpMethods.First(); resourceData.ActionName = actionDescriptor?.ActionName; resourceData.ControllerName = actionDescriptor?.ControllerName; resourceData.RouteTemplate = routeAttribute?.Template; resourceData.DisplayName = action.DisplayName; actionResources.Add(new ActionResource() { Data = resourceData, Key = actionDescriptor.GetSecurityKey() }); } } return await Task.FromResult(actionResources);}参考 ASP .NET Core 源码中的 AuthorizeFilter:
https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Authorization/AuthorizeFilter.cs
AuthorizeFilter 中有一个 OnAuthorizationAsync 的方法,首先获取 policy 执行器,然后执行了认证,接着执行授权,根据授权结果修改 AuthorizationFilterContext,我们权限主要是 Forbidden 的结果,返回 403 即可
public virtual async Task OnAuthorizationAsync(AuthorizationFilterContext context){ if (context == null) { throw new ArgumentNullException(nameof(context)); } if (!context.IsEffectivePolicy(this)) { return; } // IMPORTANT: Changes to authorization logic should be mirrored in security's AuthorizationMiddleware var effectivePolicy = await GetEffectivePolicyAsync(context); if (effectivePolicy == null) { return; } var policyEvaluator = context.HttpContext.RequestServices.GetRequiredService<IPolicyEvaluator>(); var authenticateResult = await policyEvaluator.AuthenticateAsync(effectivePolicy, context.HttpContext); // Allow Anonymous skips all authorization if (HasAllowAnonymous(context)) { return; } var authorizeResult = await policyEvaluator.AuthorizeAsync(effectivePolicy, authenticateResult, context.HttpContext, context); if (authorizeResult.Challenged) { context.Result = new ChallengeResult(effectivePolicy.AuthenticationSchemes.ToArray()); } else if (authorizeResult.Forbidden) { context.Result = new ForbidResult(effectivePolicy.AuthenticationSchemes.ToArray()); }}DynamicAuthorizeFilter 继承自 AuthorizeFilter,只获取 ControllerActionDescriptor 类型的 action,如果 permissions 中不包含 actionKey 则返回 403
using Microsoft.AspNetCore.Mvc;using Microsoft.AspNetCore.Mvc.Authorization;using Microsoft.AspNetCore.Mvc.Controllers;using Microsoft.AspNetCore.Mvc.Filters;namespace DotNetNB.Security.ActionAccess{ public class DynamicAuthorizeFilter : AuthorizeFilter { public override async Task OnAuthorizationAsync(AuthorizationFilterContext context) { var actionDescriptor = context.ActionDescriptor as ControllerActionDescriptor; if (actionDescriptor == null) { return; } base.OnAuthorizationAsync(context); if (context.Result != null) { return; } var permissions = context.HttpContext.User.Claims.Where(c => c.Type == Core.ClaimsTypes.Permission); var actionKey = actionDescriptor.GetSecurityKey(); var values = permissions.Select(p => p.Value); if (!values.Contains(actionKey)) { context.Result = new ForbidResult(); } } }}在 ClaimsTypes 中增加一个 Permission 类型的 ClaimsType
namespace DotNetNB.Security.Core{ public static class ClaimsTypes { public const string Permission = "Permission"; }}为了避免重复代码,添加一个 GetSecurityKey 的扩展方法
using Microsoft.AspNetCore.Mvc.Controllers;using Microsoft.AspNetCore.Mvc.Internal;namespace DotNetNB.Security.ActionAccess;public static string GetSecurityKey(this ControllerActionDescriptor descriptor){ var httpMethod = descriptor.EndpointMetadata.FirstOrDefault(m => m is HttpMethodMetadata) as HttpMethodMetadata; return string.Format($"{descriptor?.ControllerName}-{descriptor?.ActionName}-{httpMethod.HttpMethods.First()}");}这样就完成了 DotNetNB.Security.ActionAccess 模块的 ActionResourceProvider 和 DynamicAuthorizeFilter
https://github.com/MingsonZheng/dotnetnb.security
https://appsqsyiqlk5791.h5.xiaoeknow.com/v1/course/video/v_5f39bdb8e4b01187873136cf?type=2

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。
如有任何疑问,请与我联系 (MingsonZheng@outlook.com) 。