1. 程式人生 > >ABP中的攔截器之AuditingInterceptor

ABP中的攔截器之AuditingInterceptor

esc oot on() users resource self ini intercept 日誌文件

  在上面兩篇介紹了ABP中的ValidationInterceptor之後,我們今天來看看ABP中定義的另外一種Interceptor即為AuditingInterceptor,顧名思義就是一種審計相關的作用,整個過程也是從AbpBootstrapper中的AddInterceptorRegistrars方法開始的,在這個方法中首先對AuditingInterceptor進行初始化操作,具體的來看看下面的代碼。

internal static class AuditingInterceptorRegistrar
    {
        public static void Initialize(IIocManager iocManager)
        {
            iocManager.IocContainer.Kernel.ComponentRegistered += (key, handler) =>
            {
                if (!iocManager.IsRegistered<IAuditingConfiguration>())
                {
                    return;
                }

                var auditingConfiguration = iocManager.Resolve<IAuditingConfiguration>();

                if (ShouldIntercept(auditingConfiguration, handler.ComponentModel.Implementation))
                {
                    handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(AuditingInterceptor)));
                }
            };
        }
        
        private static bool ShouldIntercept(IAuditingConfiguration auditingConfiguration, Type type)
        {
            if (auditingConfiguration.Selectors.Any(selector => selector.Predicate(type)))
            {
                return true;
            }

            if (type.GetTypeInfo().IsDefined(typeof(AuditedAttribute), true))
            {
                return true;
            }

            if (type.GetMethods().Any(m => m.IsDefined(typeof(AuditedAttribute), true)))
            {
                return true;
            }

            return false;
        }
    }

  在這個方法內部,首先將整個ABP中唯一的IoCManager作為參數傳遞到裏面,然後訂閱依賴註入容器的ComponentRegister事件,這裏訂閱的函數有兩個參數,一個是key,另外一個是IHandle的接口,這段代碼意思是說當Ioc中有組件被註冊的時候(也就是往Ioc添加某個類型的時候), 就會觸發ComponentRegister事件,然後執行事件的訂閱操作,在這個訂閱事件處理中首先判斷當前的ABP中是否註冊過IAuditingConfiguration這個接口,如果沒有註冊過那麽就直接返回了,如果對前面的文章有過印象的話,你就知道這個註冊的過程是沿著下面的過程來進行的:UseAbp--》InitializeAbp(app)--》abpBootstrapper.Initialize()--》IocManager.IocContainer.Install(new AbpCoreInstaller());在最後執行AbpCoreInstaller的時候,在這個類中有一個Install方法,在這個裏面就對ABP中常用的接口都註冊並註入到容器中了。

 internal class AbpCoreInstaller : IWindsorInstaller
    {
        public void Install(IWindsorContainer container, IConfigurationStore store)
        {
            container.Register(
                Component.For<IUnitOfWorkDefaultOptions, UnitOfWorkDefaultOptions>().ImplementedBy<UnitOfWorkDefaultOptions>().LifestyleSingleton(),
                Component.For<INavigationConfiguration, NavigationConfiguration>().ImplementedBy<NavigationConfiguration>().LifestyleSingleton(),
                Component.For<ILocalizationConfiguration, LocalizationConfiguration>().ImplementedBy<LocalizationConfiguration>().LifestyleSingleton(),
                Component.For<IAuthorizationConfiguration, AuthorizationConfiguration>().ImplementedBy<AuthorizationConfiguration>().LifestyleSingleton(),
                Component.For<IValidationConfiguration, ValidationConfiguration>().ImplementedBy<ValidationConfiguration>().LifestyleSingleton(),
                Component.For<IFeatureConfiguration, FeatureConfiguration>().ImplementedBy<FeatureConfiguration>().LifestyleSingleton(),
                Component.For<ISettingsConfiguration, SettingsConfiguration>().ImplementedBy<SettingsConfiguration>().LifestyleSingleton(),
                Component.For<IModuleConfigurations, ModuleConfigurations>().ImplementedBy<ModuleConfigurations>().LifestyleSingleton(),
                Component.For<IEventBusConfiguration, EventBusConfiguration>().ImplementedBy<EventBusConfiguration>().LifestyleSingleton(),
                Component.For<IMultiTenancyConfig, MultiTenancyConfig>().ImplementedBy<MultiTenancyConfig>().LifestyleSingleton(),
                Component.For<ICachingConfiguration, CachingConfiguration>().ImplementedBy<CachingConfiguration>().LifestyleSingleton(),
                Component.For<IAuditingConfiguration, AuditingConfiguration>().ImplementedBy<AuditingConfiguration>().LifestyleSingleton(),
                Component.For<IBackgroundJobConfiguration, BackgroundJobConfiguration>().ImplementedBy<BackgroundJobConfiguration>().LifestyleSingleton(),
                Component.For<INotificationConfiguration, NotificationConfiguration>().ImplementedBy<NotificationConfiguration>().LifestyleSingleton(),
                Component.For<IEmbeddedResourcesConfiguration, EmbeddedResourcesConfiguration>().ImplementedBy<EmbeddedResourcesConfiguration>().LifestyleSingleton(),
                Component.For<IAbpStartupConfiguration, AbpStartupConfiguration>().ImplementedBy<AbpStartupConfiguration>().LifestyleSingleton(),
                Component.For<IEntityHistoryConfiguration, EntityHistoryConfiguration>().ImplementedBy<EntityHistoryConfiguration>().LifestyleSingleton(),
                Component.For<ITypeFinder, TypeFinder>().ImplementedBy<TypeFinder>().LifestyleSingleton(),
                Component.For<IAbpPlugInManager, AbpPlugInManager>().ImplementedBy<AbpPlugInManager>().LifestyleSingleton(),
                Component.For<IAbpModuleManager, AbpModuleManager>().ImplementedBy<AbpModuleManager>().LifestyleSingleton(),
                Component.For<IAssemblyFinder, AbpAssemblyFinder>().ImplementedBy<AbpAssemblyFinder>().LifestyleSingleton(),
                Component.For<ILocalizationManager, LocalizationManager>().ImplementedBy<LocalizationManager>().LifestyleSingleton()
                );
        }
    }

  有了上面的分析,你大概知道了這個繼承自IAuditingConfiguration接口的AuditingConfiguration會作為唯一的實例註入到ABP中的容器內部。在這之後會執行一個非常重要的函數ShouldIntercept,這個方法用來判斷哪些形式的能夠最終執行當前的Interceptor,在這個方法中,後面兩種都比較好理解,如果一個類比如說繼承自IApplicationService的一個應用服務類在其頂部或者內部的方法中添加了AuditAttribute自定義CustomerAttribute,那麽就會執行審計過程,如果是定義在類的級別中那麽該類中的所有請求的方法都會執行後面的審計AuditingInterceptor,如果不是定義在類級別上,而是定義在類裏面的方法中,那麽只有請求了該方法的時候才會執行當前審計操作。這裏面不太好理解的就是第一種判斷方式。在ABP中默認添加了一個Selector,這個實在AbpKenelModule的PreInitialize()中添加的。

private void AddAuditingSelectors()
        {
            Configuration.Auditing.Selectors.Add(
                new NamedTypeSelector(
                    "Abp.ApplicationServices",
                    type => typeof(IApplicationService).IsAssignableFrom(type)
                )
            );
        }

  這個也是比較好理解的,就是所有從IApplicationService繼承的類都會默認添加AuditingInterceptor,另外我們在我們自己的項目中的PreInitialize()方法中自定義規則,這個是ABP中對外擴展的一種方式。在了解完這些後你應該完全了解ABP中默認對哪些類型進行AuditingInterceptor攔截了。

  接下來的重點就是去分析 AuditingInterceptor這個Interceptor這個具體的攔截器到底是怎樣工作的。

internal class AuditingInterceptor : IInterceptor
    {
        private readonly IAuditingHelper _auditingHelper;

        public AuditingInterceptor(IAuditingHelper auditingHelper)
        {
            _auditingHelper = auditingHelper;
        }

        public void Intercept(IInvocation invocation)
        {
            if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Auditing))
            {
                invocation.Proceed();
                return;
            }

            if (!_auditingHelper.ShouldSaveAudit(invocation.MethodInvocationTarget))
            {
                invocation.Proceed();
                return;
            }

            var auditInfo = _auditingHelper.CreateAuditInfo(invocation.TargetType, invocation.MethodInvocationTarget, invocation.Arguments);

            if (invocation.Method.IsAsync())
            {
                PerformAsyncAuditing(invocation, auditInfo);
            }
            else
            {
                PerformSyncAuditing(invocation, auditInfo);
            }
        }

        private void PerformSyncAuditing(IInvocation invocation, AuditInfo auditInfo)
        {
            var stopwatch = Stopwatch.StartNew();

            try
            {
                invocation.Proceed();
            }
            catch (Exception ex)
            {
                auditInfo.Exception = ex;
                throw;
            }
            finally
            {
                stopwatch.Stop();
                auditInfo.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds);
                _auditingHelper.Save(auditInfo);
            }
        }

        private void PerformAsyncAuditing(IInvocation invocation, AuditInfo auditInfo)
        {
            var stopwatch = Stopwatch.StartNew();

            invocation.Proceed();

            if (invocation.Method.ReturnType == typeof(Task))
            {
                invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithFinally(
                    (Task) invocation.ReturnValue,
                    exception => SaveAuditInfo(auditInfo, stopwatch, exception)
                    );
            }
            else //Task<TResult>
            {
                invocation.ReturnValue = InternalAsyncHelper.CallAwaitTaskWithFinallyAndGetResult(
                    invocation.Method.ReturnType.GenericTypeArguments[0],
                    invocation.ReturnValue,
                    exception => SaveAuditInfo(auditInfo, stopwatch, exception)
                    );
            }
        }

        private void SaveAuditInfo(AuditInfo auditInfo, Stopwatch stopwatch, Exception exception)
        {
            stopwatch.Stop();
            auditInfo.Exception = exception;
            auditInfo.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds);

            _auditingHelper.Save(auditInfo);
        }
    }

  在這個裏面,當我們將要被執行Auditing方法之前,首先會執行AuditingInterceptor 類中的Intercept方法,在這個方法體內部,首先也是執行AbpCrossCuttingConcerns.IsApplied方法,在這個方法中首先會判斷這個執行當前方法所屬的類是否是從IAvoidDuplicateCrossCuttingConcerns接口繼承,如果從這個接口繼承的話,那麽將執行方法的所屬的類轉換為IAvoidDuplicateCrossCuttingConcerns類型,然後再看當前接口中定義的List<string>類型的AppliedCrossCuttingConcerns對象中是否已經包含AbpAuditing字符串,如果已經包含那麽就直接執行攔截的方法,然後就返回。這裏需要特別註意的是在整個ABP系統中只有一個ApplicationService繼承自IAvoidDuplicateCrossCuttingConcerns這個接口,所以在我們的系統中,只有繼承自ApplicationService類的類中的方法被攔截器攔截時才會執行上面的過程。這個分析過程其實和之前的ValidationInterceptor中的分析過程是一致的,所以這裏就不再贅述,直接拿出結果。

  ABP中利用Asp.Net Core中的過濾器的特性其實也定義了一組Filter,這個可以看下面的代碼。在Asp.Net Core中執行ConfigureServices的時候會執行AddAbp方法在這個方法中會執行對MvcOptions的一些操作。

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

  我們來看看這個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());
        }
    }

  在這個方法中,ABP系統默認添加了6中類型的Filter,這其中就包括AbpAuditActionFilter,在這個Filter中,我們來看看到底做了些什麽?

 public class AbpAuditActionFilter : IAsyncActionFilter, ITransientDependency
    {
        private readonly IAbpAspNetCoreConfiguration _configuration;
        private readonly IAuditingHelper _auditingHelper;

        public AbpAuditActionFilter(IAbpAspNetCoreConfiguration configuration, IAuditingHelper auditingHelper)
        {
            _configuration = configuration;
            _auditingHelper = auditingHelper;
        }

        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            if (!ShouldSaveAudit(context))
            {
                await next();
                return;
            }

            using (AbpCrossCuttingConcerns.Applying(context.Controller, AbpCrossCuttingConcerns.Auditing))
            {
                var auditInfo = _auditingHelper.CreateAuditInfo(
                    context.ActionDescriptor.AsControllerActionDescriptor().ControllerTypeInfo.AsType(),
                    context.ActionDescriptor.AsControllerActionDescriptor().MethodInfo,
                    context.ActionArguments
                );

                var stopwatch = Stopwatch.StartNew();

                try
                {
                    var result = await next();
                    if (result.Exception != null && !result.ExceptionHandled)
                    {
                        auditInfo.Exception = result.Exception;
                    }
                }
                catch (Exception ex)
                {
                    auditInfo.Exception = ex;
                    throw;
                }
                finally
                {
                    stopwatch.Stop();
                    auditInfo.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds);
                    await _auditingHelper.SaveAsync(auditInfo);
                }
            }
        }

        private bool ShouldSaveAudit(ActionExecutingContext actionContext)
        {
            return _configuration.IsAuditingEnabled &&
                   actionContext.ActionDescriptor.IsControllerAction() &&
                   _auditingHelper.ShouldSaveAudit(actionContext.ActionDescriptor.GetMethodInfo(), true);
        }
    }

  在繼承自ApplicationService的類中的方法執行之前,首先會執行OnActionExecutionAsync方法,在這個方法中首先判斷一些基礎的條件,這些通常都是一些默認的設置,在判斷完這些類型以後,就會執行下面的這些方法,在這個方法中會將默認的字符串AbpAuditing

名稱類型
? (obj as IAvoidDuplicateCrossCuttingConcerns) {Castle.Proxies.SelfAppServiceroxy} Abp.Application.Services.IAvoidDuplicateCrossCuttingConcerns {Castle.Proxies.SelfAppServiceProxy}

寫入到一個默認的List<string>類型中,這個具體過程可以參考上面的分析,在AbpCrossCuttingConcerns.Applying中第一個參數最為關鍵,那麽這個context.Controller(也就是上圖中obj對應的參數)到底指的是什麽呢?這裏我們執行一個繼承自ApplicationService中的SelfAppService中的一個方法時,我們通過調試發現最終的類型是Castle.Proxies.SelfAppServiceProxy 類型,如果對這個還不太理解,可以這麽理解其實就是我們自定義的SelfAppService這個繼承自ApplicationService 類的類型。

  再在後面就是通過構造函數註入的IAuditingHelper對象來創建一個auditInfo,這個創建的類是最關鍵的,ABP系統中有一個默認的IAuditingHelper的實現,我們來重點看看這個類中到底做了些什麽?

 public class AuditingHelper : IAuditingHelper, ITransientDependency
    {
        public ILogger Logger { get; set; }
        public IAbpSession AbpSession { get; set; }
        public IAuditingStore AuditingStore { get; set; }

        private readonly IAuditInfoProvider _auditInfoProvider;
        private readonly IAuditingConfiguration _configuration;
        private readonly IUnitOfWorkManager _unitOfWorkManager;
        private readonly IAuditSerializer _auditSerializer;

        public AuditingHelper(
            IAuditInfoProvider auditInfoProvider,
            IAuditingConfiguration configuration,
            IUnitOfWorkManager unitOfWorkManager,
            IAuditSerializer auditSerializer)
        {
            _auditInfoProvider = auditInfoProvider;
            _configuration = configuration;
            _unitOfWorkManager = unitOfWorkManager;
            _auditSerializer = auditSerializer;

            AbpSession = NullAbpSession.Instance;
            Logger = NullLogger.Instance;
            AuditingStore = SimpleLogAuditingStore.Instance;
        }

        public bool ShouldSaveAudit(MethodInfo methodInfo, bool defaultValue = false)
        {
            if (!_configuration.IsEnabled)
            {
                return false;
            }

            if (!_configuration.IsEnabledForAnonymousUsers && (AbpSession?.UserId == null))
            {
                return false;
            }

            if (methodInfo == null)
            {
                return false;
            }

            if (!methodInfo.IsPublic)
            {
                return false;
            }

            if (methodInfo.IsDefined(typeof(AuditedAttribute), true))
            {
                return true;
            }

            if (methodInfo.IsDefined(typeof(DisableAuditingAttribute), true))
            {
                return false;
            }

            var classType = methodInfo.DeclaringType;
            if (classType != null)
            {
                if (classType.GetTypeInfo().IsDefined(typeof(AuditedAttribute), true))
                {
                    return true;
                }

                if (classType.GetTypeInfo().IsDefined(typeof(DisableAuditingAttribute), true))
                {
                    return false;
                }

                if (_configuration.Selectors.Any(selector => selector.Predicate(classType)))
                {
                    return true;
                }
            }

            return defaultValue;
        }

        public AuditInfo CreateAuditInfo(Type type, MethodInfo method, object[] arguments)
        {
            return CreateAuditInfo(type, method, CreateArgumentsDictionary(method, arguments));
        }

        public AuditInfo CreateAuditInfo(Type type, MethodInfo method, IDictionary<string, object> arguments)
        {
            var auditInfo = new AuditInfo
            {
                TenantId = AbpSession.TenantId,
                UserId = AbpSession.UserId,
                ImpersonatorUserId = AbpSession.ImpersonatorUserId,
                ImpersonatorTenantId = AbpSession.ImpersonatorTenantId,
                ServiceName = type != null
                    ? type.FullName
                    : "",
                MethodName = method.Name,
                Parameters = ConvertArgumentsToJson(arguments),
                ExecutionTime = Clock.Now
            };

            try
            {
                _auditInfoProvider.Fill(auditInfo);
            }
            catch (Exception ex)
            {
                Logger.Warn(ex.ToString(), ex);
            }

            return auditInfo;
        }

        public void Save(AuditInfo auditInfo)
        {
            using (var uow = _unitOfWorkManager.Begin(TransactionScopeOption.Suppress))
            {
                AuditingStore.Save(auditInfo);
                uow.Complete();
            }
        }

        public async Task SaveAsync(AuditInfo auditInfo)
        {
            using (var uow = _unitOfWorkManager.Begin(TransactionScopeOption.Suppress))
            {
                await AuditingStore.SaveAsync(auditInfo);
                await uow.CompleteAsync();
            }
        }

        private string ConvertArgumentsToJson(IDictionary<string, object> arguments)
        {
            try
            {
                if (arguments.IsNullOrEmpty())
                {
                    return "{}";
                }

                var dictionary = new Dictionary<string, object>();

                foreach (var argument in arguments)
                {
                    if (argument.Value != null && _configuration.IgnoredTypes.Any(t => t.IsInstanceOfType(argument.Value)))
                    {
                        dictionary[argument.Key] = null;
                    }
                    else
                    {
                        dictionary[argument.Key] = argument.Value;
                    }
                }

                return _auditSerializer.Serialize(dictionary);
            }
            catch (Exception ex)
            {
                Logger.Warn(ex.ToString(), ex);
                return "{}";
            }
        }

        private static Dictionary<string, object> CreateArgumentsDictionary(MethodInfo method, object[] arguments)
        {
            var parameters = method.GetParameters();
            var dictionary = new Dictionary<string, object>();

            for (var i = 0; i < parameters.Length; i++)
            {
                dictionary[parameters[i].Name] = arguments[i];
            }

            return dictionary;
        }
    }

  在這個類中重點的就是CreateAuditInfo這個方法,這個方法會創建一個AuditInfo對象,然後往這個對象中填充一些系統的常見的一些信息,比如:TenantId、UserId、ServiceName等等一系類的常用對象,我們來看看AuditInfo這個對象包含哪些重要的東西吧?

 public class AuditInfo
    {
        /// <summary>
        /// TenantId.
        /// </summary>
        public int? TenantId { get; set; }
        
        /// <summary>
        /// UserId.
        /// </summary>
        public long? UserId { get; set; }

        /// <summary>
        /// ImpersonatorUserId.
        /// </summary>
        public long? ImpersonatorUserId { get; set; }

        /// <summary>
        /// ImpersonatorTenantId.
        /// </summary>
        public int? ImpersonatorTenantId { get; set; }

        /// <summary>
        /// Service (class/interface) name.
        /// </summary>
        public string ServiceName { get; set; }
        
        /// <summary>
        /// Executed method name.
        /// </summary>
        public string MethodName { get; set; }

        /// <summary>
        /// Calling parameters.
        /// </summary>
        public string Parameters { get; set; }

        /// <summary>
        /// Start time of the method execution.
        /// </summary>
        public DateTime ExecutionTime { get; set; }

        /// <summary>
        /// Total duration of the method call.
        /// </summary>
        public int ExecutionDuration { get; set; }

        /// <summary>
        /// IP address of the client.
        /// </summary>
        public string ClientIpAddress { get; set; }
        
        /// <summary>
        /// Name (generally computer name) of the client.
        /// </summary>
        public string ClientName { get; set; }

        /// <summary>
        /// Browser information if this method is called in a web request.
        /// </summary>
        public string BrowserInfo { get; set; }

        /// <summary>
        /// Optional custom data that can be filled and used.
        /// </summary>
        public string CustomData { get; set; }
        
        /// <summary>
        /// Exception object, if an exception occurred during execution of the method.
        /// </summary>
        public Exception Exception { get; set; }

        public override string ToString()
        {
            var loggedUserId = UserId.HasValue
                                   ? "user " + UserId.Value
                                   : "an anonymous user";

            var exceptionOrSuccessMessage = Exception != null
                ? "exception: " + Exception.Message
                : "succeed";

            return $"AUDIT LOG: {ServiceName}.{MethodName} is executed by {loggedUserId} in {ExecutionDuration} ms from {ClientIpAddress} IP address with {exceptionOrSuccessMessage}.";
        }
    }

  在創建完這個用來保存AuditingInfo的AuditInfo對象後,接下來的事情就比較明了了,就是創建一個StopWatch用於記錄當前方法執行的時間,後面再用一個try、catch、finally來包裝執行的方法,捕獲錯誤,並將當前的Exception捕獲並賦值給剛才創建的auditInfo中,完成這個步驟之後就是將整個AuditInfo進行保存從而方便我們對當前方法進行排錯和優化效率的操作了。

  在執行完最重要的步驟之後就是如何保存這些重要的信息了,我們來一起看看這個重要的步驟都做了些什麽吧?

public async Task SaveAsync(AuditInfo auditInfo)
        {
            using (var uow = _unitOfWorkManager.Begin(TransactionScopeOption.Suppress))
            {
                await AuditingStore.SaveAsync(auditInfo);
                await uow.CompleteAsync();
            }
        }

  在這個函數裏面,執行一個AuditingStore的SaveAsync的方法,這是一個異步方法,用來對最終的信息進行保存。ABP中默認是采用日誌的方式來將當前的auditInfo轉化為字符串然後保存到日誌文件中的,當然我們也可以將當前的信息保存到數據庫中的,這樣我們就能夠查看更多的系統運行狀態的信息了,下面是一張具體的截圖我們來看看。

技術分享圖片

  最後,點擊這裏返回整個ABP系列的主目錄。

ABP中的攔截器之AuditingInterceptor