这个部分我打算用上下两个部分来将整个结构来讲完,在我们读ABP中的代码之后我们一直有一个疑问?在ABP中为什么要定义Interceptor和Filter,甚至这两者之间我们都能找到一些对应关系,比如:AuthorizationInterceptor和AbpAuthorizationFilter,AuditingInterceptor和AbpAuditActionFilter,甚至这些代码的实现也是调用相同的接口和实现?那么ABP为什么要采用这种方式呢?在前面的章节中我已经充分介绍过了ABP中的各种Interceptor,这一部分我们将重点来理解ABP中的另外一种类型Filter,在理解本篇文章之前,建议先对Asp.Net Core中的Filter有一个清晰的理解,这里建议看微软官方的文档使自己对整个Filter有一个清晰的认识,或者读这篇博客来理解到底什么是Asp.Net Core 中的Filter。

  在有了前面的预备知识以后我们就可以来探讨ABP中的Filter到底是怎么一回事了,还是和以前的分析思路一样,我们来看看整个Filter在ABP中是怎样添加并运行的,在我们的项目代码中当我们在Asp.Net Core中使用ABP框架时,我们要做的第一步就是在Startup类的ConfigureServices方法中将我们的ABP添加到DI容器中,ConfigureServices是用来将服务注册到DI容器用的,在Asp.Net Core的Demo程序中我们经常可以看到下面的代码:

// ...
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
    }
}

  在这里services就是一个DI容器。此例把MVC的服务注册到DI容器,等到需要用到MVC服务时,才从DI容器取得物件实例。在我们的ABP中整个实现也是按照这个思路来进行的,首先是在ConfigureServices方法中我们调用AddAbp方法,在这个方法的主要作用按照ABP中的注释是:Integrates ABP to AspNet Core.主要就是将ABP集成到整个Asp.Net Core中,我们来简要看看这个AddAbp方法到底做了些什么?

 /// <summary>
        /// Integrates ABP to AspNet Core.
        /// </summary>
        /// <typeparam name="TStartupModule">Startup module of the application which depends on other used modules. Should be derived from <see cref="AbpModule"/>.</typeparam>
        /// <param name="services">Services.</param>
        /// <param name="optionsAction">An action to get/modify options</param>
        public static IServiceProvider AddAbp<TStartupModule>(this IServiceCollection services, [CanBeNull] Action<AbpBootstrapperOptions> optionsAction = null)
            where TStartupModule : AbpModule
        {
            var abpBootstrapper = AddAbpBootstrapper<TStartupModule>(services, optionsAction);

            ConfigureAspNetCore(services, abpBootstrapper.IocManager);

            return WindsorRegistrationHelper.CreateServiceProvider(abpBootstrapper.IocManager.IocContainer, services);
        }

  主要就是做了三件事:1 初始化整个AbpBootstrapper并创建唯一的实例。2 配置Asp.Net Core中的一些重要的服务及参数。 3 将我们之前注册到DI容器中的所有IServiceCollection都添加到ABP中使用的Windsor Castel容器中来接管了 ASP.NET Core 自带的IOC容器。而我们今天要讨论和比较的两个部分就包含在这几个过程中,为整个ABP框架中添加Interceptor在第一个过程中完成,而我们今天要讲的ABP中的Filter就是包含在第二个过程中,那么我们先来看看第二个过程。

  //Configure MVC
  services.Configure<MvcOptions>(mvcOptions =>
    {
         mvcOptions.AddAbp(services);
    });

  在第二个过程我们只选取和添加Filter相关的代码部分,这部分的重点在于mvcOptions.AddAbp的方法,我们来看看里面的实现。

 internal static class AbpMvcOptionsExtensions
    {
        public static void AddAbp(this MvcOptions options, IServiceCollection services)
        {
            AddConventions(options, services);
            AddFilters(options);
            AddModelBinders(options);
        }

        private static void AddConventions(MvcOptions options, IServiceCollection services)
        {
            options.Conventions.Add(new AbpAppServiceConvention(services));
        }

        private static void AddFilters(MvcOptions options)
        {
            options.Filters.AddService(typeof(AbpAuthorizationFilter));
            options.Filters.AddService(typeof(AbpAuditActionFilter));
            options.Filters.AddService(typeof(AbpValidationActionFilter));
            options.Filters.AddService(typeof(AbpUowActionFilter));
            options.Filters.AddService(typeof(AbpExceptionFilter));
            options.Filters.AddService(typeof(AbpResultFilter));
        }

        private static void AddModelBinders(MvcOptions options)
        {
            options.ModelBinderProviders.Insert(0, new AbpDateTimeModelBinderProvider());
        }
    }

  看到了吧,这里我们向MvcOptions对应的Filter里面添加了6中不同的Filter,看到这个我们就知道了接下来我们就要对这几种Filter进行分析和总结。

  一  AbpAuthorizationFilter

  在分析这部分之前我们可以和之前讲到的AuthorizationInterceptor来做一个对比,因为我们在分析代码的时候发现这两个方法大部分的代码都是重复的,在AuthorizationInterceptor中能够对某个类型或者这个类型中的Public或者是NonPublic方法是否定义了自定义的AbpAuthorize或者RequiresFeature属性,只有定义了这些属性才会进行拦截操作。两个实现的内部都是调用IAuthorizationHelper中定义的Authorize方法,然后执行CheckFeatures和CheckPermissions这两个过程,这两个过程在之前的部分有介绍这里就不再赘述。这里面我们发现在AbpAuthorizationFilter中只会拦截ControllerAction,所以可以这么理解AbpAuthorizationFilter只要用于拦截Controller中定义的一些方法,如果不是一个控制器方法则直接返回,而AuthorizationInterceptor则通过 Castle Windsor Interceptor 来验证普通类型的方法,来检测当前用户是否有权限进行调用。所以这里我们需要进行区分。

  另外在AbpAuthorizationFilter这一部分中,我们可以看到整个过程使用了两个Catch来捕获特定的异常,然后进行特定的处理,这些处理包括记录日志、触发异常事件、以及对ObjectResult的返回类型采用AjaxResponse对象进行封装信息等一系列处理,这里我们可以看一下源代码的处理过程。

public class AbpAuthorizationFilter : IAsyncAuthorizationFilter, ITransientDependency
{
    public ILogger Logger { get; set; }
 
    // 权限验证类,这个才是真正针对权限进行验证的对象
    private readonly IAuthorizationHelper _authorizationHelper;
    // 异常包装器主要是用来封装没有授权时返回的错误信息
    private readonly IErrorInfoBuilder _errorInfoBuilder;
    // 事件总线处理器在这里用于触发一个未授权请求引发的事件,用户可以监听此事件来进行自己的处理
    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);
            }
        }
    }
}  

  二  AbpAuditActionFilter

  在使用审计ActionFilter之前,我们也建议先看看之前介绍AuditingInterceptor的文章,通过对比你发现这两个部分核心也是相同的,其内部都是调用IAuditingHelper中定义的CreateAuditInfo来创建审计日志,那么我们着重看看这两者在实现方式上面有哪些不同的地方,在方法(AuditingInterceptor中的Intercept)调用之前,首先也是通过AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Auditing)方法来验证当前方法是否已经应用过审计功能,原因也好理解同一个方法不应该被AuditingInterceptor和AbpAuditActionFilter中的方法拦截两次,所以这个判断很好理解就是用于判断当前调用方法是否已经被应用过审计功能了,如果应用过则直接跳过去。我们来看看IsApplied方法中到底做了些什么?

 public static bool IsApplied([NotNull] object obj, [NotNull] string concern)
        {
            if (obj == null)
            {
                throw new ArgumentNullException(nameof(obj));
            }

            if (concern == null)
            {
                throw new ArgumentNullException(nameof(concern));
            }

            return (obj as IAvoidDuplicateCrossCuttingConcerns)?.AppliedCrossCuttingConcerns.Contains(concern) ?? false;
        }

  在这个方法的内部首先将当前当前类型转换为IAvoidDuplicateCrossCuttingConcerns接口形式,如果转换成功则判断接口中定义的AppliedCrossCuttingConcerns字符串List是否已经包含了AbpCrossCuttingConcerns.Auditing=“AbpAuditing”这个常量,那么在ABP框架中哪种类型的对象能够转换为IAvoidDuplicateCrossCuttingConcerns接口形式呢?我们发现在ABP中默认有两种类型继承自IAvoidDuplicateCrossCuttingConcerns接口,一种是ApplicationService另外一种就是DynamicApiController<T>,第二种不太常见,但是第一种在应用层每一个应用服务对象都要继承自ApplicationService,所以说应用层对象都能够转换成IAvoidDuplicateCrossCuttingConcerns形式,那么我们通过阅读源码发现这两个作用的范围是不同的,AbpAuditActionFilter主要用于审计Controller层中添加了Audited属性的公共并且非匿名登录的一些方法的审计工作日志输出工作,而AuditingInterceptor中的Intercept方法主要用于其他一些添加了Audited属性的公共方法并且非匿名登录的一些方法的普通类型的方法或类型的审计工作,通过这里相信你对这两者已经有一个清晰的认识啦,接下来我们就是看一看IAuditingHelper中做了些什么,其实里面主要是将当前执行方法的TeantId、UserID、ServiceName、名称、类型、参数、耗时、Exception、TotalMilliseconds等参数自动保存到数据库中,从而方便我们来查询这些方法的执行情况。

  三  AbpValidationActionFilter

  这个部分的内容在ValidationInterceptor中的上篇下篇有过详细的对比,这里不再赘述。

  后面的AbpUowActionFilter、AbpExceptionFilter、以及AbpResultFilter将在下一篇中具体讲到,当前篇主要就是包含这些内容。

       最后,点击这里返回整个ABP系列的主目录。


 
                    
            
                

分类:

技术点:

相关文章: