目錄
- 開發任務
- 程式碼實現
開發任務
- DotNetNB.Security.Core:定義 core,models,Istore;實現 default memory store
- DotNetNB.Security.ActionAccess:掃描 action;新增 action authorize filter;新增整合方式
程式碼實現
對於一個 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:
建立 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
GitHub原始碼連結:
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) 。