ABP Framework-Permission&Authorization源码解析
目录
https://abp.io/docs/6.0/Authorization
前提了解
https://learn.microsoft.com/zh-cn/aspnet/core/security/authorization/policies?view=aspnetcore-8.0
ABP基于Asp.Net Core的授权检查与权限声明体系,使用[AuthorizeAttribute],而对于Asp.Net Core的权限声明简要描述下:
Controller/Action上加AuthorizeAttribute,使用Role或Policy来限制方法的访问。
请求到达时,Authorize判断前提默认为鉴权通过的标记了IsAuthenticated。其次会按照Role或Policy,从当前请求的HttpContext中或者查询数据库/服务中,拿到有关信息,如果符合Role或Policy则授权检查通过。
请求方法,执行逻辑,返回结果。
一般场景很少直接使用Role,并且此文基于ABP的实践,采用ABP中的权限定义来作为Policy,具体便是,方法上标记Policy=PermissionDefinition.XXXName。在授权检查时,查询当前人的角色或自身具备了该权限,则授权检查通过。
Version
6.0.3
Package
Volo.Abp.Authorization
Volo.Abp.Authorization.Abstractions
//独立模块
Volo.Abp.PermissionManagement.*
权限定义
权限定义树
在各垂直业务的独立模块中,一般会定义各自的权限定义树,比如User/Tenant等,也比如订单模块,地址模块等,都会在模块内构建好自身的权限定义树。
public static class WorkSpacePermissions
{
public const string GroupName = "WorkSpaceManagement";
public static class Projects
{
public const string Default = GroupName + ".Projects";
public const string Create = Default + ".Create";
public const string Update = Default + ".Update";
public const string Delete = Default + ".Delete";
public const string ManageTypes = Default + ".ManageTypes";
public static class Members
{
public const string Default = Projects.Default + ".Members";
public const string Create = Default + ".Create";
public const string Update = Default + ".Update";
public const string Delete = Default + ".Delete";
public const string ManageRoles = Default + ".ManageRoles";
public const string ManageInvites = Default + ".ManageInvites";
}
}
}
public class WorkSpacePermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var projectsPermission = workSpaceGroup.AddPermission(WorkSpacePermissions.Projects.Default);
projectsPermission.AddChild(WorkSpacePermissions.Projects.Create);
projectsPermission.AddChild(WorkSpacePermissions.Projects.Update);
projectsPermission.AddChild(WorkSpacePermissions.Projects.Delete);
projectsPermission.AddChild(WorkSpacePermissions.Projects.ManageTypes);
var projectMembersPermission = projectsPermission.AddChild(WorkSpacePermissions.Projects.Members.Default);
projectMembersPermission.AddChild(WorkSpacePermissions.Projects.Members.Create);
projectMembersPermission.AddChild(WorkSpacePermissions.Projects.Members.Update);
projectMembersPermission.AddChild(WorkSpacePermissions.Projects.Members.Delete);
projectMembersPermission.AddChild(WorkSpacePermissions.Projects.Members.ManageRoles);
projectMembersPermission.AddChild(WorkSpacePermissions.Projects.Members.ManageInvites);
}
}
可以构建多级权限(顶级Group不参与权限)只用来权限分组。在ABP6.0版本,该部分权限定义不会持久化到数据库中,此处仅以6.0版本为主(8.0会持久化)。
IPermissionDefinitionProvider中定义了如下三个接口,但使用频繁的还是Define
public interface IPermissionDefinitionProvider
{
void PreDefine(IPermissionDefinitionContext context);
void Define(IPermissionDefinitionContext context);
void PostDefine(IPermissionDefinitionContext context);
}
权限定义汇总
PermissionDefinitionProvider会在启动时在Module初始化时完成扫描注册到AbpPermissionOptions的PermissionDefinitionProviders属性中。
在权限检查应用时,不直接从AbpPermissionOptions取,而是抽象一层IPermissionDefinitionManager,其中封装了取权限和取Group的方法。简要类图如下。
权限声明
对于Controller/Action上,和Asp.Net Core的授权方式相同,使用AuthorizeAttribute特性,加上权限定义,该定义过程不涉及ABP源码,简单示例如下。
[HttpPost]
[Authorize(WorkSpacePermissions.Projects.Create)]
public async Task<ProjectDto> CreateAsync(CreateProjectDto input)
{
return await _projectAppService.CreateAsync(input);
}
当在ApplicationService中使用时,同样如此,ABP没有额外封一个AuthorizeAttribute。
[Authorize(WorkSpacePermissions.Projects)]
public class ProjectAppService : ApplicationService, IProjectAppService
{
[Authorize(WorkSpacePermissions.Projects.Create)]
public Task<ProjectDto> CreateAsync(CreateProjectDto input)
{
...
}
}
还有一种场景是在方法内执行权限声明
public class ProjectAppService : ApplicationService, IProjectAppService
{
public Task<ProjectDto> CreateAsync(CreateProjectDto input)
{
var result = await AuthorizationService
.AuthorizeAsync(WorkSpacePermissions.Projects.Create);
if (result.Succeeded == false)
{
//throw exception
throw new AbpAuthorizationException("...");
}
}
}
三种方式有所不同,处理的时机,出发点,方式不同,最终使用的Handler方式相同。
对于Controlller/Action来讲,是由Asp.Net Core的授权中间件来处理,调用Handler经过一系列判断执行授权检查。
而对于在ApplicationService或Method上标记,则借助拦截器完成,经过拦截,最终使用上Handler,具体见后续小节。
对于方法内的授权检查,实际上还是利用Asp.Net Core提供的AuthorizationService,和中间件中内部用的方式相同,最终走到Handler中处理。
授权检查
服务注册
当请求到达,进入Controller/Action时,授权检查开始工作,此处先提及AbpAuthorizationModule.cs中服务注册内容,因为请求时依赖该部分注册的服务。
第一部分为拦截器注册,是ApplicationService/Method上如果标记了AuthorizeAttribute的处理部分。
第二部分中,注册了AuthorizationCore,这是授权检查必不可少的服务,其次加入了Abp封装的PermissionRequirementHandler,具体的授权检查基于该Handler。还有一个AuthorizationPolicyProvider,这种方式是Asp.Net Core提供的动态Policy转换,使用多,扩展方便。Abp封装了一个PolicyProvider便于将Policy转换为PermissionRequirement,再经PermissionRequirementHandler处理。
第三部分中,是具体权限值的提供,是否拥有权限的数据来源,内部一般是读取数据库拿到值。
Policy转换
如上一节提到了AuthorizationPolicyProvider,其中将Policy转换成Requirement,在ABP封装的类中,此处查看其简化后核心源码。
public class AbpAuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider, IAbpAuthorizationPolicyProvider, ITransientDependency
{
public override async Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
// PolicyName转换到AuthorizationPolicy,前置判断
var policy = await base.GetPolicyAsync(policyName);
if (policy != null)
{
return policy;
}
// 检查该权限是否在权限定义树中
var permission = _permissionDefinitionManager.GetOrNull(policyName);
if (permission != null)
{
// 转换Policy到ABP的PermissionRequirement
var policyBuilder = new AuthorizationPolicyBuilder(Array.Empty<string>());
policyBuilder.Requirements.Add(new PermissionRequirement(policyName));
return policyBuilder.Build();
}
return null;
}
}
AbpAuthorizationPolicyProvider相关类图与依赖如下。
该类完成了对Policy到Requirement的动态转换,接下来便是Handler对Requirement的授权检查。
授权检查
ABP封装了两个RequirementHandler(PermissionRequirementHandler,PermissionsRequirementHandler),其中一个是对于多个权限的处理,此处不考虑,只关注PermissionRequirementHandler,其中核心是IPermissionCheck。
该部分类图如下
其中RemotePermissionChecker用于Mvc独立项目远程授权检查使用,此处不考虑,而对于AlwaysAllowPermissionChecker,则更多在测试项目中使用,以隔离权限检查。如下简化实际代码关注核心部分。
public class PermissionChecker : IPermissionChecker, ITransientDependency
{
public virtual async Task<bool> IsGrantedAsync(
ClaimsPrincipal claimsPrincipal,
string name)
{
// 权限是否存在于权限定义树中
var permission = PermissionDefinitionManager.Get(name);
if (!permission.IsEnabled)
{
return false;
}
var isGranted = false;
var context = new PermissionValueCheckContext(permission, claimsPrincipal);
// 遍历初始化时注册好的权限值提供来源
foreach (var provider in PermissionValueProviderManager.ValueProviders)
{
// 进入到提供者内执行是否被赋权判断
var result = await provider.CheckAsync(context);
if (result == PermissionGrantResult.Granted)
{
isGranted = true;
}
else if (result == PermissionGrantResult.Prohibited)
{
return false;
}
}
return isGranted;
}
}
参照同样的方式,如果有场景需要支持其他权限检查,可以扩展。
权限值提供者
PermissionCheck最终进入到PermissionValueProvider,大多数是查询数据库执行匹配,当然也可以扩展想要的场景。但PermissionCheck不直接和PermissionValueProvider交互,而是由PermissionValueProviderManager来统筹管理众多的PermissionValueProvider。该部分类图简要如下。
在具体的PermissionValueProvider中实际上调用IPermissionStore来得到是否具有分配的权限,默认情况下其实现是PermissionStore从数据库表中读取对应权限。如下简要查看下UserPermissionValueProvider。
public class UserPermissionValueProvider : PermissionValueProvider
{
public const string ProviderName = "U";
public override string Name => ProviderName;
public override async Task<PermissionGrantResult> CheckAsync(PermissionValueCheckContext context)
{
// 从请求信息中获取和分配权限有关的标识
var userId = context.Principal?.FindFirst(AbpClaimTypes.UserId)?.Value;
if (userId == null)
{
return PermissionGrantResult.Undefined;
}
// 根据U,UserId及权限名查询授权表中是否存在记录
return await PermissionStore.IsGrantedAsync(context.Permission.Name, Name, userId)
? PermissionGrantResult.Granted
: PermissionGrantResult.Undefined;
}
}
AOP拦截器
该部分内容是在ApplicationService/Method上标记AuthorizeAttribute,在代码中当请求到该部分接口时,会执行授权检查,其最终实现还是调用的前几节内容,只是触发时机,位置不同。
在服务注册一节中AbpAuthorizationModule注册了AuthorizationInterceptorRegistrar,其内部判断了需要拦截的类型,当满足条件后,则加上拦截器AuthorizationInterceptor。
在AuthorizationInterceptor拦截器内部,调用IMethodInvocationAuthorizationService,其内部则是调用的IAbpAuthorizationPolicyProvider和IAbpAuthorizationService,便是回到了授权检查中间件中所用到的服务了。
public class MethodInvocationAuthorizationService : IMethodInvocationAuthorizationService, ITransientDependency
{
private readonly IAbpAuthorizationPolicyProvider _abpAuthorizationPolicyProvider;
private readonly IAbpAuthorizationService _abpAuthorizationService;
public MethodInvocationAuthorizationService(
IAbpAuthorizationPolicyProvider abpAuthorizationPolicyProvider,
IAbpAuthorizationService abpAuthorizationService)
{
_abpAuthorizationPolicyProvider = abpAuthorizationPolicyProvider;
_abpAuthorizationService = abpAuthorizationService;
}
public async Task CheckAsync(MethodInvocationAuthorizationContext context)
{
// 匿名检查
if (AllowAnonymous(context))
{
return;
}
// 从PolicyProvider中得到AuthorizationPolicy
var authorizationPolicy = await AuthorizationPolicy.CombineAsync(
_abpAuthorizationPolicyProvider,
GetAuthorizationDataAttributes(context.Method)
);
if (authorizationPolicy == null)
{
return;
}
// 授权检查
await _abpAuthorizationService.CheckAsync(authorizationPolicy);
}
protected virtual bool AllowAnonymous(MethodInvocationAuthorizationContext context)
{
return context.Method.GetCustomAttributes(true).OfType<IAllowAnonymous>().Any();
}
protected virtual IEnumerable<IAuthorizeData> GetAuthorizationDataAttributes(MethodInfo methodInfo)
{
var attributes = methodInfo
.GetCustomAttributes(true)
.OfType<IAuthorizeData>();
if (methodInfo.IsPublic && methodInfo.DeclaringType != null)
{
attributes = attributes
.Union(
methodInfo.DeclaringType
.GetCustomAttributes(true)
.OfType<IAuthorizeData>()
);
}
return attributes;
}
}
此处细化一下AbpAuthorizationService.CheckAsync,其实现内部最终调用到Asp.Net Core中的AuthorizationService.AuthorizeAsync,最后再进入到AuthorizationHandler中。
CheckAsync() //MethodInvocationAuthorizationService.cs
abpAuthorizationService.CheckAsync() //AbpAuthorizationServiceExtensions.cs
authorizationService.IsGrantedAsync() //AbpAuthorizationServiceExtensions.cs
authorizationService.AuthorizeAsync()//AbpAuthorizationServiceExtensions.cs
authorizationService.AuthorizeAsync() //Microsoft.AspNetCore.Authorization.cs
PermissionRequirementHandler.HandleRequirementAsync() //PermissionRequirementHandler.cs
该部分类图简要如下
手动权限检查
在方法内部当想要检查是否拥有某权限时,还可以直接使用AuthorizationService来检查。
public async Task CreateAsync(CreateAuthorDto input)
{
var result = await AuthorizationService
.AuthorizeAsync("Author_Management_Create_Books");
if (result.Succeeded == false)
{
//throw exception
throw new AbpAuthorizationException("...");
}
//continue to the normal flow...
}
Microsoft.Asp.Net Core.Authorization.AuthorizationService在AppService中内置好了,因此直接使用即可。
public abstract class ApplicationService :
IApplicationService,
IAvoidDuplicateCrossCuttingConcerns,
IValidationEnabled,
IUnitOfWorkEnabled,
IAuditingEnabled,
IGlobalFeatureCheckingEnabled,
ITransientDependency
{
protected IAuthorizationService AuthorizationService => LazyServiceProvider.LazyGetRequiredService<IAuthorizationService>();
}
最终还是进入到PermissionRequirementHandler中或者自定义的AuthorizationHandler中。
扩展
授权这块Asp.Net Core本身提供了很多扩展,PolicyProvider和AuthorizationHandler很常用。对于ABP中,提供了一些额外的扩展。
自定义PermissionCheck,类似RemotePermisionCheck,例如当权限检查在BFF层又不能访问数据库时,可以实现远程调用服务来实现检查。
自定义PermissionValueProvider,提供想要的扩展。因场景需要,我这之前扩展了下菜单的权限值,对菜单进行控制,只允许具有权限和特定菜单的才能通过权限判定。类似场景都可以扩展。
2024-02-20,望技术有成后能回来看见自己的脚步。