0.简介

Abp 本身集成了一套权限验证体系,通过 ASP.NET Core 的过滤器与 Castle 的拦截器进行拦截请求,并进行权限验证。在 Abp 框架内部,权限分为两块,一个是功能(Feature),一个是权限项(Permission),在更多的时候两者仅仅是概念不同而已,大体处理流程还是一样的。

由于 Abp 本身是针对多租户架构进行设计的,功能是相对于租户而言,比如针对 A 租户他每月的短信发送配额为 10000 条,而针对 B 租户其配额为 5000 条,可能 C 租户该功能都没有开通。

本篇文章仅针对基本的验证机制进行解析,后续文章会进行详解。

0.1 验证流程图

1.启动流程

1.1 流程图

1.2 代码流程

首先在注入 Abp 框架的时候,通过注入过滤器一起将权限验证过滤器进行了注入。

internal static class AbpMvcOptionsExtensions
{
// ... 其他代码 private static void AddFilters(MvcOptions options)
{
// ... 其他注入的过滤器
options.Filters.AddService(typeof(AbpAuthorizationFilter));
// ... 其他注入的过滤器
} // ... 其他代码
}

Abp 除了拦截验证 API 接口,同时也通过 Castle Windsor Interceptor 来验证普通类型的方法,来检测当前用户是否有权限进行调用。拦截器的注册则是存放在 AbpBootstrapper 对象初始化的时候,通过 AddInterceptorRegistrars() 方法注入 Abp 自带的拦截器对象。

private AbpBootstrapper([NotNull] Type startupModule, [CanBeNull] Action<AbpBootstrapperOptions> optionsAction = null)
{
Check.NotNull(startupModule, nameof(startupModule)); var options = new AbpBootstrapperOptions();
optionsAction?.Invoke(options); // 其他初始化代码 // 判断用户在启用 Abp 框架的是时候是否禁用了所有的拦截器
if (!options.DisableAllInterceptors)
{
// 初始化拦截器
AddInterceptorRegistrars();
}
} private void AddInterceptorRegistrars()
{
// 参数验证拦截器注册
ValidationInterceptorRegistrar.Initialize(IocManager);
// 审计信息记录拦截器注册
AuditingInterceptorRegistrar.Initialize(IocManager);
// 实体变更追踪拦截器注册
EntityHistoryInterceptorRegistrar.Initialize(IocManager);
// 工作单元拦截器注册
UnitOfWorkRegistrar.Initialize(IocManager);
// 授权拦截器注册
AuthorizationInterceptorRegistrar.Initialize(IocManager);
}

Abp 通过注入过滤器与拦截器就能够从源头验证并控制权限校验逻辑,以上就是 Abp 在启动时所做的操作。

2.代码分析

总体来说,Abp 针对权限的验证就是拦截+检测,整体思路即是这样,只是实现可能略微复杂,请耐心往下看。

2.1 权限拦截器与权限过滤器

首先我们从入口点开始分析代码,在上一节我们说过 Abp 通过拦截器与过滤器来实现权限的拦截与处理,那么在其内部是如何进行处理的呢?

其实很简单,在权限拦截器与权限过滤器的内部实现都使用了 IAuthorizationHelperAuthorizeAsync() 方法来进行权限校验。

2.1.1 权限过滤器代码实现

public class AbpAuthorizationFilter : IAsyncAuthorizationFilter, ITransientDependency
{
public ILogger Logger { get; set; } // 权限验证类,这个才是真正针对权限进行验证的对象
private readonly IAuthorizationHelper _authorizationHelper;
// 异常包装器,这个玩意儿在我的《[Abp 源码分析]十、异常处理》有讲,主要是用来封装没有授权时返回的错误信息
private readonly IErrorInfoBuilder _errorInfoBuilder;
// 事件总线处理器,同样在我的《[Abp 源码分析]九、事件总线》有讲,在这里用于触发一个未授权请求引发的事件,用户可以监听此事件来进行自己的处理
private readonly IEventBus _eventBus; // 构造注入
public AbpAuthorizationFilter(
IAuthorizationHelper authorizationHelper,
IErrorInfoBuilder errorInfoBuilder,
IEventBus eventBus)
{
_authorizationHelper = authorizationHelper;
_errorInfoBuilder = errorInfoBuilder;
_eventBus = eventBus;
Logger = NullLogger.Instance;
} public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
// 如果注入了 IAllowAnonymousFilter 过滤器则允许所有匿名请求
if (context.Filters.Any(item => item is IAllowAnonymousFilter))
{
return;
} // 如果不是一个控制器方法则直接返回
if (!context.ActionDescriptor.IsControllerAction())
{
return;
} // 开始使用 IAuthorizationHelper 来进行权限校验
try
{
await _authorizationHelper.AuthorizeAsync(
context.ActionDescriptor.GetMethodInfo(),
context.ActionDescriptor.GetMethodInfo().DeclaringType
);
}
// 如果是未授权异常的处理逻辑
catch (AbpAuthorizationException ex)
{
// 记录日志
Logger.Warn(ex.ToString(), ex); // 触发异常事件
_eventBus.Trigger(this, new AbpHandledExceptionData(ex)); // 如果接口的返回类型为 ObjectResult,则采用 AjaxResponse 对象进行封装信息
if (ActionResultHelper.IsObjectResult(context.ActionDescriptor.GetMethodInfo().ReturnType))
{
context.Result = new ObjectResult(new AjaxResponse(_errorInfoBuilder.BuildForException(ex), true))
{
StatusCode = context.HttpContext.User.Identity.IsAuthenticated
? (int) System.Net.HttpStatusCode.Forbidden
: (int) System.Net.HttpStatusCode.Unauthorized
};
}
else
{
context.Result = new ChallengeResult();
}
}
// 其他异常则显示为内部异常信息
catch (Exception ex)
{
Logger.Error(ex.ToString(), ex); _eventBus.Trigger(this, new AbpHandledExceptionData(ex)); if (ActionResultHelper.IsObjectResult(context.ActionDescriptor.GetMethodInfo().ReturnType))
{
context.Result = new ObjectResult(new AjaxResponse(_errorInfoBuilder.BuildForException(ex)))
{
StatusCode = (int) System.Net.HttpStatusCode.InternalServerError
};
}
else
{
//TODO: How to return Error page?
context.Result = new StatusCodeResult((int)System.Net.HttpStatusCode.InternalServerError);
}
}
}
}

2.1.2 权限拦截器初始化绑定

权限拦截器在 Abp 框架初始化完成的时候就开始监听了组件注册事件,只要被注入的类型实现了 AbpAuthorizeAttribute 特性与 RequiresFeatureAttribute 特性都会被注入 AuthorizationInterceptor 拦截器。

internal static class AuthorizationInterceptorRegistrar
{
public static void Initialize(IIocManager iocManager)
{
// 监听 DI 组件注册事件
iocManager.IocContainer.Kernel.ComponentRegistered += Kernel_ComponentRegistered;
} private static void Kernel_ComponentRegistered(string key, IHandler handler)
{
// 判断注入的类型是否符合要求
if (ShouldIntercept(handler.ComponentModel.Implementation))
{
// 符合要求,针对该组件添加权限拦截器
handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(AuthorizationInterceptor)));
}
} private static bool ShouldIntercept(Type type)
{
if (SelfOrMethodsDefinesAttribute<AbpAuthorizeAttribute>(type))
{
return true;
} if (SelfOrMethodsDefinesAttribute<RequiresFeatureAttribute>(type))
{
return true;
} return false;
} private static bool SelfOrMethodsDefinesAttribute<TAttr>(Type type)
{
// 判断传入的 Type 有定义 TAttr 类型的特性
if (type.GetTypeInfo().IsDefined(typeof(TAttr), true))
{
return true;
} // 或者说,该类型的所有公开的方法是否有方法标注了 TAttr 类型的特性
return type
.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Any(m => m.IsDefined(typeof(TAttr), true));
}
}

2.1.3 权限拦截器实现

Abp 框架针对权限拦截器的实现则是简单了许多,只是在被拦截的方法在执行的时候,会直接使用 IAuthorizationHelper 进行权限验证。

public class AuthorizationInterceptor : IInterceptor
{
private readonly IAuthorizationHelper _authorizationHelper; public AuthorizationInterceptor(IAuthorizationHelper authorizationHelper)
{
_authorizationHelper = authorizationHelper;
} public void Intercept(IInvocation invocation)
{
// 使用 IAuthorizationHelper 进行权限验证
_authorizationHelper.Authorize(invocation.MethodInvocationTarget, invocation.TargetType);
invocation.Proceed();
}
}

2.2 权限特性

在 Abp 框架里面定义了两组特性,第一个是 AbpMvcAuthorizeAttribute ,适用于 MVC 控制器,它是直接继承了 ASP .NET Core 自带的权限验证特性 AuthorizeAttribute,当控制器或者控制器内部的方法标注了该特性,就会进入之前 Abp 定义的权限过滤器 AbpAuthorizationFilter 内部。

第二种特性则是 AbpAuthorizeAttribute ,该特性适用于应用服务层,也就是实现了 IApplicationService 接口的类型所使用的。

它们两个的内部定义基本一样,传入一个或者多哦个具体的权限项,以便给 IAuthorizationHelper 作验证使用。

在 Abp 框架内部,每一个权限其实就是一个字符串,比如说用户资料新增,是一个权限,那么你可以直接创建一个 "Administration.UserManagement.CreateUser" 字符作为其权限项,那么代码示例就如下:

[AbpAuthorize("Administration.UserManagement.CreateUser")]
public void CreateUser(CreateUserInput input)
{
// 如果用户没有 Administration.UserManagement.CreateUser 权限,则不会进入到本方法
}

下面是 AbpAuthorizeAttribute 权限特性的定义,另外一个 MVC 权限特性定义也是一样的:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class AbpAuthorizeAttribute : Attribute, IAbpAuthorizeAttribute
{
// 特性拥有的权限项集合
public string[] Permissions { get; } // 用于确定是否需要验证用户是否拥有 Permission 数组内所有权限项,如果为 True 则用户需要拥有所有权限才能够操作接口,如果为 False 的话,用户只要拥有其中一个权限项则可以通过验证,默认值为:False
public bool RequireAllPermissions { get; set; } public AbpAuthorizeAttribute(params string[] permissions)
{
Permissions = permissions;
}
}

权限特性一般都会打在你的控制器/应用服务层的类定义,或者方法之上,当你为你的 API 接口标注了权限特性,那么当前请求的用户没有所需要的权限,则一律会被拦截器/过滤器阻止请求。

2.3 权限验证

当如果用户请求的方法或者控制器是标注了授权特性的话,都会通过 IAuthorizationHelper 进行验证,它一共有两个公开方法。

public interface IAuthorizationHelper
{
// 判断用户是否拥有一组权限特性所标注的权限
Task AuthorizeAsync(IEnumerable<IAbpAuthorizeAttribute> authorizeAttributes); // 判断用户是否拥有,被调用的方法所标注的权限
Task AuthorizeAsync(MethodInfo methodInfo, Type type);
}

在其默认的实现当中,注入了两个相对重要的组件,第一个是 IAbpSession,它是 Abp 框架定义的用户会话状态,如果当前用户处于登录状态的时候,其内部必定有值,在这里主要用于判断用户是否登录。

第二个则是 IPermissionChecker ,它则是用于具体的检测逻辑,如果说 IAuthorizationHelper 是用来提供权限验证的工具,那么 IPermissionChecker 就是权限验证的核心,在 IPermissionChecker 内部则是真正的对传入的权限进行了验证逻辑。

IPermissionChecker 本身只有两个方法,都返回的 bool 值,有权限则为 true 没有则为 false,其接口定义如下:

// 权限检测器
public interface IPermissionChecker
{
// 传入一个权限项的值,判断当前用户是否拥有该权限
Task<bool> IsGrantedAsync(string permissionName); // 传入一个用户标识,判断该用户是否拥有制定的权限项
Task<bool> IsGrantedAsync(UserIdentifier user, string permissionName);
}

可以看到 Abp 框架本身针对于设计来说,都考虑了各个组件的可替换性与扩展性,你可以随时通过替换 IAuthorizationHelper 或者是 IPermissionChecker 的实现来达到自己想要的效果,这点值得我们在编写代码的时候学习。

说了这么多,下面我们来看一下 IAuthorizationHelper 的具体实现吧:

public class AuthorizationHelper : IAuthorizationHelper, ITransientDependency
{
public IAbpSession AbpSession { get; set; }
public IPermissionChecker PermissionChecker { get; set; }
public IFeatureChecker FeatureChecker { get; set; }
public ILocalizationManager LocalizationManager { get; set; } private readonly IFeatureChecker _featureChecker;
private readonly IAuthorizationConfiguration _authConfiguration; public AuthorizationHelper(IFeatureChecker featureChecker, IAuthorizationConfiguration authConfiguration)
{
_featureChecker = featureChecker;
_authConfiguration = authConfiguration;
AbpSession = NullAbpSession.Instance;
PermissionChecker = NullPermissionChecker.Instance;
LocalizationManager = NullLocalizationManager.Instance;
} public virtual async Task AuthorizeAsync(IEnumerable<IAbpAuthorizeAttribute> authorizeAttributes)
{
// 判断是否启用了授权系统,没有启用则直接跳过不做验证
if (!_authConfiguration.IsEnabled)
{
return;
} // 如果当前的用户会话状态其 SessionId 没有值,则说明用户没有登录,抛出授权验证失败异常
if (!AbpSession.UserId.HasValue)
{
throw new AbpAuthorizationException(
LocalizationManager.GetString(AbpConsts.LocalizationSourceName, "CurrentUserDidNotLoginToTheApplication")
);
} // 遍历所有授权特性,通过 IPermissionChecker 来验证用户是否拥有这些特性所标注的权限
foreach (var authorizeAttribute in authorizeAttributes)
{
await PermissionChecker.AuthorizeAsync(authorizeAttribute.RequireAllPermissions, authorizeAttribute.Permissions);
}
} // 授权过滤器与授权拦截器调用的方法,传入一个方法定义与方法所在的类的类型
public virtual async Task AuthorizeAsync(MethodInfo methodInfo, Type type)
{
// 检测产品功能
await CheckFeatures(methodInfo, type);
// 检测权限
await CheckPermissions(methodInfo, type);
} protected virtual async Task CheckFeatures(MethodInfo methodInfo, Type type)
{
var featureAttributes = ReflectionHelper.GetAttributesOfMemberAndType<RequiresFeatureAttribute>(methodInfo, type); if (featureAttributes.Count <= 0)
{
return;
} foreach (var featureAttribute in featureAttributes)
{
// 检查当前用户是否启用了被调用方法标注上面的功能
await _featureChecker.CheckEnabledAsync(featureAttribute.RequiresAll, featureAttribute.Features);
}
} protected virtual async Task CheckPermissions(MethodInfo methodInfo, Type type)
{
// 判断是否启用了授权系统,没有启用则直接跳过不做验证
if (!_authConfiguration.IsEnabled)
{
return;
} // 判断方法或者控制器类上是否标注了匿名访问特性,如果标注了,不做权限验证
if (AllowAnonymous(methodInfo, type))
{
return;
} // 获得方法和类上面定义的所有权限特性数组
var authorizeAttributes =
ReflectionHelper
.GetAttributesOfMemberAndType(methodInfo, type)
.OfType<IAbpAuthorizeAttribute>()
.ToArray(); // 如果一个都不存在,跳过验证
if (!authorizeAttributes.Any())
{
return;
} // 传入所有权限特性,调用另外一个重载方法,使用 IPermissionChecker 针对这些特性进行具体验证
await AuthorizeAsync(authorizeAttributes);
} private static bool AllowAnonymous(MemberInfo memberInfo, Type type)
{
return ReflectionHelper
.GetAttributesOfMemberAndType(memberInfo, type)
.OfType<IAbpAllowAnonymousAttribute>()
.Any();
}
}

看完上面你似乎并没有看到哪儿有抛出 AbpAuthorizationException 的地方,这是因为 Abp 给 IPermissionChecker 添加了一个扩展方法,叫做 AuthorizeAsync() ,看他的具体实现你就知道,它在这个扩展方法里面才真正调用了 IPermissionChecker.IsGrantedAsync() 方法进行权限验证。

public static async Task AuthorizeAsync(this IPermissionChecker permissionChecker, bool requireAll, params string[] permissionNames)
{
// 这里还是调用的一个扩展方法,其内部是遍历传入的权限项集合,针对每一个权限进行检测
if (await IsGrantedAsync(permissionChecker, requireAll, permissionNames))
{
return;
} // 这儿呢就是本地化权限的名称,用于抛出异常的时候给前端展示用的,里面提列了你缺少的权限项有哪些
var localizedPermissionNames = LocalizePermissionNames(permissionChecker, permissionNames); if (requireAll)
{
throw new AbpAuthorizationException(
string.Format(
L(
permissionChecker,
"AllOfThesePermissionsMustBeGranted",
"Required permissions are not granted. All of these permissions must be granted: {0}"
),
string.Join(", ", localizedPermissionNames)
)
);
}
else
{
throw new AbpAuthorizationException(
string.Format(
L(
permissionChecker,
"AtLeastOneOfThesePermissionsMustBeGranted",
"Required permissions are not granted. At least one of these permissions must be granted: {0}"
),
string.Join(", ", localizedPermissionNames)
)
);
}
}

如果你感觉自己快被绕晕了,也不必惊慌...因为 IPermissionChecker 本身只能针对单个权限进行检查,所以这里通过扩展了 IPermissionChecker 方法,使其能够一次检验一个集合而已。

3.结语

本篇文章主要解析了 Abp 框架针对权限验证所做的基本操作,整体思路还是十分简单的,在 Abp 基本框架没有涉及到用户与角色的具体权限控制,这部分的内容是存放在 Abp.Zero 模块当中的,下一篇文章将会结合 Abp.Zero 来进行更加详细的讲解权限与功能的实现。

4.点此跳转到总目录

[Abp 源码分析]十一、权限验证的更多相关文章

  1. ABP源码分析十一:Timing

    Timing这个简单实用的功能主要用于以统一的方式表示时间.因为ABP中有大量的module,还支持自定义module,所以将时间统一表示为local时间(默认)或utc时间是必要的. IClockP ...

  2. [Abp 源码分析]十七、ASP.NET Core 集成

    0. 简介 整个 Abp 框架最为核心的除了 Abp 库之外,其次就是 Abp.AspNetCore 库了.虽然 Abp 本身是可以用于控制台程序的,不过那样的话 Abp 就基本没什么用,还是需要集合 ...

  3. ABP源码分析一:整体项目结构及目录

    ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...

  4. [Abp 源码分析]十二、多租户体系与权限验证

    0.简介 承接上篇文章我们会在这篇文章详细解说一下 Abp 是如何结合 IPermissionChecker 与 IFeatureChecker 来实现一个完整的多租户系统的权限校验的. 1.多租户的 ...

  5. ABP源码分析二十一:Feature

    Feature是什么?Feature就是对function的分类方法,其与function的关系就比如Role和User的关系一样. ABP中Feature具有以下属性: 其中最重要的属性是name, ...

  6. ABP源码分析三十一:ABP.AutoMapper

    这个模块封装了Automapper,使其更易于使用. 下图描述了改模块涉及的所有类之间的关系. AutoMapAttribute,AutoMapFromAttribute和AutoMapToAttri ...

  7. ABP源码分析四十一:ZERO的Audit,Setting,Background Job

    AuditLog: 继承自Entity<long>的实体类.封装AuditLog的信息. AuditingStore: 实现了IAuditingStore接口,实现了将AuditLog的信 ...

  8. [Abp 源码分析]零、文章目录

    0.系列文章目录 一.Abp 框架启动流程分析 二.模块系统 三.依赖注入 四.模块配置 五.系统设置 六.工作单元的实现 七.仓储与 Entity Framework Core 八.缓存管理 九.事 ...

  9. ABP源码分析十八:UI Inputs

    以下图中描述的接口和类都在Abp项目的Runtime/Validation, UI/Inputs目录下的.在当前版本的ABP(0.83)中这些接口和类并没有实际使用到.阅读代码时可以忽略,无需浪费时间 ...

随机推荐

  1. 自己动手写Redis客户端(C#实现)2 - SET请求和状态回复(set)

    Redis请求协议的一般形式: *<参数数量> CR LF $<参数 的字节数量> CR LF <参数 的数据> CR LF ... $<参数 N 的字节数量 ...

  2. C++11 带来的新特性 (2)—— 统一初始化(Uniform Initialization)

    1 统一初始化(Uniform Initialization) 在C++ 11之前,所有对象的初始化方式是不同的,经常让写代码的我们感到困惑.C++ 11努力创造一个统一的初始化方式. 其语法是使用{ ...

  3. 手把手教你从ESXI部署到vSphere web Client管理控制

    作为实验环境,一台物理机即可 既然是实验环境,那么首先把这个物理机装成ESXI6.5的宿主机并配置网络系统 第二步骤就是在ESXI上面导入OVF文件,注册一台虚机,作为数据管理中心 第三步骤就是基于这 ...

  4. P1522 牛的旅行 Cow Tours floyed

    题目描述 农民 John的农场里有很多牧区.有的路径连接一些特定的牧区.一片所有连通的牧区称为一个牧场.但是就目前而言,你能看到至少有两个牧区通过任何路径都不连通.这样,Farmer John就有多个 ...

  5. 我对PMO的理解(持续更新)

    PMO的价值 为项目管理提供方法上的指导,对项目的实施过程提供监督.评价. PMO应该从哪方面着手建立管理体系 如何量化工作 如何考评工作 如何激励员工 如何进行人工.成本核算 如何进行进度跟踪与控制 ...

  6. WPF中的ObservableCollection数据绑定

    使用时ObservableCollection必须使用get set属性声明,才能成功的绑定到控件.

  7. preventDefault()、stopPropagation()、return false 的区别

    preventDefault() e.preventDefault()阻止浏览器默认事件 stopPropagation() e.stopPropagation()阻止冒泡 return false ...

  8. The frist email to myself by python

    昨天用python模块给自己发了第一封电子邮件,真是激动 今天完善了一下. code: import smtplib from email.mime.text import MIMEText//ema ...

  9. 百度TTS的来由

    #### https://home-assistant.io/components/tts.baidu/#### https://github.com/charleyzhu/HomeAssistant ...

  10. 三色GDOI

    前面说点什么.. 翻了翻别人的游记.. 发现自己从来没写过gdoi的游记.. 那就.. 一起写完它吧.. GDOI2015 肇庆 没啥印象,只有复联2的首映.. 那时候真的是什么都不懂.. 就是外出打 ...