0.简介

Abp 框架为我们自带了审计日志功能,审计日志可以方便地查看每次请求接口所耗的时间,能够帮助我们快速定位到某些性能有问题的接口。除此之外,审计日志信息还包含有每次调用接口时客户端请求的参数信息,客户端的 IP 与客户端使用的浏览器。有了这些数据之后,我们就可以很方便地复现接口产生 BUG 时的一些环境信息。

当然如果你脑洞更大的话,可以根据这些数据来开发一个可视化的图形界面,方便开发与测试人员来快速定位问题。

PS:

如果使用了 Abp.Zero 模块则自带的审计记录实现是存储到数据库当中的,但是在使用 EF Core + MySQL(EF Provider 为 Pomelo.EntityFrameworkCore.MySql) 在高并发的情况下会有数据库连接超时的问题,这块推荐是重写实现,自己采用 Redis 或者其他存储方式。

如果需要禁用审计日志功能,则需要在任意模块的预加载方法(PreInitialize()) 当中增加如下代码关闭审计日志功能。

public class XXXStartupModule
{
public override PreInitialize()
{
// 禁用审计日志
Configuration.Auditing.IsEnabled = false;
}
}

1.启动流程

审计组件与参数校验组件一样,都是通过 MVC 过滤器与 Castle 拦截器来实现记录的。也就是说,在每次调用接口/方法时都会进入 过滤器/拦截器 并将其写入到数据库表 AbpAuditLogs 当中。

其核心思想十分简单,就是在执行具体接口方法的时候,先使用 StopWatch 对象来记录执行完一个方法所需要的时间,并且还能够通过 HttpContext 来获取到一些客户端的关键信息。

2.1 过滤器注入

同上一篇文章所讲的一样,过滤器是在 AddAbp() 方法内部的 ConfigureAspNetCore() 方法注入的。

private static void ConfigureAspNetCore(IServiceCollection services, IIocResolver iocResolver)
{
// ... 其他代码 //Configure MVC
services.Configure<MvcOptions>(mvcOptions =>
{
mvcOptions.AddAbp(services);
}); // ... 其他代码
}

而下面就是过滤器的注入方法:

internal static class AbpMvcOptionsExtensions
{
public static void AddAbp(this MvcOptions options, IServiceCollection services)
{
// ... 其他代码
AddFilters(options);
// ... 其他代码
} // ... 其他代码 private static void AddFilters(MvcOptions options)
{
// ... 其他过滤器注入 // 注入审计日志过滤器
options.Filters.AddService(typeof(AbpAuditActionFilter)); // ... 其他过滤器注入
} // ... 其他代码
}

2.2 拦截器注入

注入拦截器的地方与 DTO 自动验证的拦截器的位置一样,都是在 AbpBootstrapper 对象被构造的时候进行注册。

public class AbpBootstrapper : IDisposable
{
private AbpBootstrapper([NotNull] Type startupModule, [CanBeNull] Action<AbpBootstrapperOptions> optionsAction = null)
{
// ... 其他代码 if (!options.DisableAllInterceptors)
{
AddInterceptorRegistrars();
}
} // ... 其他代码 // 添加各种拦截器
private void AddInterceptorRegistrars()
{
ValidationInterceptorRegistrar.Initialize(IocManager);
AuditingInterceptorRegistrar.Initialize(IocManager);
EntityHistoryInterceptorRegistrar.Initialize(IocManager);
UnitOfWorkRegistrar.Initialize(IocManager);
AuthorizationInterceptorRegistrar.Initialize(IocManager);
} // ... 其他代码
}

转到 AuditingInterceptorRegistrar 的具体实现可以发现,他在内部针对于审计日志拦截器的注入是区分了类型的。

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>(); // 判断当前 DI 所注入的类型是否应该为其绑定审计日志拦截器
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;
} // 当前类型如果拥有 Audited 特性,则进行拦截器绑定
if (type.GetTypeInfo().IsDefined(typeof(AuditedAttribute), true))
{
return true;
} // 如果当前类型内部的所有方法当中有一个方法拥有 Audited 特性,则进行拦截器绑定
if (type.GetMethods().Any(m => m.IsDefined(typeof(AuditedAttribute), true)))
{
return true;
} // 都不满足则返回 false,不对当前类型进行绑定
return false;
}
}

可以看到在判断是否绑定拦截器的时候,Abp 使用了 auditingConfiguration.Selectors 的属性来进行判断,那么默认 Abp 为我们添加了哪些类型是必定有审计日志的呢?

通过代码追踪,我们来到了 AbpKernalModule 类的内部,在其预加载方法里面有一个 AddAuditingSelectors() 的方法,该方法的作用就是添加了一个针对于应用服务类型的一个选择器对象。

public sealed class AbpKernelModule : AbpModule
{
public override void PreInitialize()
{
// ... 其他代码 AddAuditingSelectors(); // ... 其他代码
} // ... 其他代码 private void AddAuditingSelectors()
{
Configuration.Auditing.Selectors.Add(
new NamedTypeSelector(
"Abp.ApplicationServices",
type => typeof(IApplicationService).IsAssignableFrom(type)
)
);
} // ... 其他代码
}

我们先看一下 NamedTypeSelector 的一个作用是什么,其基本类型定义由一个 stringFunc<Type, bool> 组成,十分简单,重点就出在这个断言委托上面。

public class NamedTypeSelector
{
// 选择器名称
public string Name { get; set; } // 断言委托
public Func<Type, bool> Predicate { get; set; } public NamedTypeSelector(string name, Func<Type, bool> predicate)
{
Name = name;
Predicate = predicate;
}
}

回到最开始的地方,当 Abp 为 Selectors 添加了一个名字为 "Abp.ApplicationServices" 的类型选择器。其断言委托的大体意思就是传入的 **type ** 参数是继承自 IApplicationService 接口的话,则返回 true,否则返回 false

这样在程序启动的时候,首先注入类型的时候,会首先进入上文所述的拦截器绑定类当中,这个时候会使用 Selectors 内部的类型选择器来调用这个集合内部的断言委托,只要这些选择器对象有一个返回 true,那么就直接与当前注入的 type 绑定拦截器。

2.代码分析

2.1 过滤器代码分析

首先查看这个过滤器的整体类型结构,一个标准的过滤器,肯定要实现 IAsyncActionFilter 接口。从下面的代码我们可以看到其注入了 IAbpAspNetCoreConfiguration 和一个 IAuditingHelper 对象。这两个对象的作用分别是判断是否记录日志,另一个则是用来真正写入日志所使用的。

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)
{
// ... 代码实现
} // ... 其他代码
}

接着看 AbpAuditActionFilter() 方法内部的实现,进入这个过滤器的时候,通过 ShouldSaveAudit() 方法来判断是否要写审计日志。

之后呢与 DTO 自动验证的过滤器一样,通过 AbpCrossCuttingConcerns.Applying() 方法为当前的对象增加了一个标识,用来告诉拦截器说我已经处理过了,你就不要再重复处理了。

再往下就是创建审计信息,执行具体接口方法,并且如果产生了异常的话,也会存放到审计信息当中。

最后接口无论是否执行成功,还是说出现了异常信息,都会将其性能计数信息同审计信息一起,通过 IAuditingHelper 存储起来。

public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// 判断是否写日志
if (!ShouldSaveAudit(context))
{
await next();
return;
} // 为当前类型打上标识
using (AbpCrossCuttingConcerns.Applying(context.Controller, AbpCrossCuttingConcerns.Auditing))
{
// 构造审计信息(AuditInfo)
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);
}
}
}

2.2 拦截器代码分析

拦截器处理时的总体思路与过滤器类似,其核心都是通过 IAuditingHelper 来创建审计信息和持久化审计信息的。只不过呢由于拦截器不仅仅是处理 MVC 接口,也会处理内部的一些类型的方法,所以针对同步方法与异步方法的处理肯定会复杂一点。

拦截器呢,我们关心一下他的核心方法 Intercept() 就行了。

public void Intercept(IInvocation invocation)
{
// 判断过滤器是否已经处理了过了
if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Auditing))
{
invocation.Proceed();
return;
} // 通过 IAuditingHelper 来判断当前方法是否需要记录审计日志信息
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);
}
} // 同步方法的处理逻辑与 MVC 过滤器逻辑相似
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);
}

这里异步方法的处理在很早之前的工作单元拦截器就有过讲述,这里就不再重复说明了。

2.3 核心的 IAuditingHelper

从代码上我们就可以看到,不论是拦截器还是过滤器都是最终都是通过 IAuditingHelper 对象来储存审计日志的。Abp 依旧为我们实现了一个默认的 AuditingHelper ,实现了其接口的所有方法。我们先查看一下这个接口的定义:

public interface IAuditingHelper
{
// 判断当前方法是否需要存储审计日志信息
bool ShouldSaveAudit(MethodInfo methodInfo, bool defaultValue = false); // 根据参数集合创建一个审计信息,一般用于拦截器
AuditInfo CreateAuditInfo(Type type, MethodInfo method, object[] arguments); // 根据一个参数字典类来创建一个审计信息,一般用于 MVC 过滤器
AuditInfo CreateAuditInfo(Type type, MethodInfo method, IDictionary<string, object> arguments); // 同步保存审计信息
void Save(AuditInfo auditInfo); // 异步保存审计信息
Task SaveAsync(AuditInfo auditInfo);
}

我们来到其默认实现 AuditingHelper 类型,先看一下其内部注入了哪些接口。

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;
// 在调用 AuditingStore 进行持久化的时候使用,创建一个工作单元
private readonly IUnitOfWorkManager _unitOfWorkManager;
// 用于序列化参数信息为 JSON 字符串
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;
} // ... 其他实现的接口
}

2.3.1 判断是否创建审计信息

首先分析一下其内部的 ShouldSaveAudit() 方法,整个方法的核心作用就是根据传入的方法类型来判定是否为其创建审计信息。

其实在这一串 if 当中,你可以发现有一句代码对方法是否标注了 DisableAuditingAttribute 特性进行了判断,如果标注了该特性,则不为该方法创建审计信息。所以我们就可以通过该特性来控制自己应用服务类,控制里面的的接口是否要创建审计信息。同理,我们也可以通过显式标注 AuditedAttribute 特性来让拦截器为这个方法创建审计信息。

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;
}

2.3.2 创建审计信息

审计信息在创建的时候,就为我们将当前调用接口时的用户信息存放在了审计信息当中,之后通过 IAuditInfoProviderFill() 方法填充了客户端 IP 与浏览器信息。

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,
// 将参数转换为 JSON 字符串
Parameters = ConvertArgumentsToJson(arguments),
ExecutionTime = Clock.Now
}; try
{
// 填充客户 IP 与浏览器信息等
_auditInfoProvider.Fill(auditInfo);
}
catch (Exception ex)
{
Logger.Warn(ex.ToString(), ex);
} return auditInfo;
}

2.4 审计信息持久化

通过上一小节我们知道了在调用审计信息保存接口的时候,实际上是调用的 IAuditingStore 所提供的 SaveAsync(AuditInfo auditInfo) 方法来持久化这些审计日志信息的。

如果你没有集成 Abp.Zero 项目的话,则使用的是默认的实现,就是简单通过 ILogger 输出审计信息到日志当中。

默认有这两种实现,至于第一种是 Abp 的单元测试项目所使用的。

这里我们就简单将一下 AuditingStore 这个实现吧,其实很简单的,就是注入了一个仓储,在保存的时候往审计日志表插入一条数据即可。

这里使用了 AuditLog.CreateFromAuditInfo() 方法将 AuditInfo 类型的审计信息转换为数据库实体,用于仓储进行插入操作。

public class AuditingStore : IAuditingStore, ITransientDependency
{
private readonly IRepository<AuditLog, long> _auditLogRepository; public AuditingStore(IRepository<AuditLog, long> auditLogRepository)
{
_auditLogRepository = auditLogRepository;
} public virtual Task SaveAsync(AuditInfo auditInfo)
{
// 向表中插入数据
return _auditLogRepository.InsertAsync(AuditLog.CreateFromAuditInfo(auditInfo));
}
}

同样,这里建议重新实现一个 AuditingStore,存储在 Redis 或者其他地方。

3. 后记

前几天发现 Abp 的团队有开了一个新坑,叫做 Abp vNext 框架,该框架全部基于 .NET Core 进行开发,而且会针对微服务项目进行专门的设计,有兴趣的朋友可以持续关注。

其 GitHub 地址为:https://github.com/abpframework/abp/

官方地址为:https://abp.io/

4.点此跳转到总目录

[Abp 源码分析]十五、自动审计记录的更多相关文章

  1. C# DateTime的11种构造函数 [Abp 源码分析]十五、自动审计记录 .Net 登陆的时候添加验证码 使用Topshelf开发Windows服务、记录日志 日常杂记——C#验证码 c#_生成图片式验证码 C# 利用SharpZipLib生成压缩包 Sql2012如何将远程服务器数据库及表、表结构、表数据导入本地数据库

    C# DateTime的11种构造函数   别的也不多说没直接贴代码 using System; using System.Collections.Generic; using System.Glob ...

  2. ABP源码分析十五:ABP中的实用扩展方法

    类名 扩展的类型 方法名 参数 作用 XmlNodeExtensions XmlNode GetAttributeValueOrNull attributeName Gets an   attribu ...

  3. ABP源码分析十九:Auditing

    审计跟踪(也叫审计日志)是与安全相关的按照时间顺序的记录,它们提供了活动序列的文档证据,这些活动序列可以在任何时间影响一个特定的操作. AuditInfo:定义如下图中需要被Audit的信息. Aud ...

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

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

  5. ABP源码分析十:Unit Of Work

    ABP以AOP的方式实现UnitOfWork功能.通过UnitOfWorkRegistrar将UnitOfWorkInterceptor在某个类被注册到IOCContainner的时候,一并添加到该类 ...

  6. ABP源码分析十二:本地化

    本文逐个分析ABP中涉及到locaization的接口和类,以及相互之间的关系.本地化主要涉及两个方面:一个是语言(Language)的管理,这部分相对简单.另一个是语言对应得本地化资源(Locali ...

  7. ABP源码分析十四:Entity的设计

    IEntity<TPrimaryKey>: 封装了PrimaryKey:Id,这是一个泛型类型 IEntity: 封装了PrimaryKey:Id,这是一个int类型 Entity< ...

  8. ABP源码分析十六:DTO的设计

    IDTO:空接口,用于标注Dto对象. ComboboxItemDto:用于combobox/list中Item的DTO NameValueDto<T>/NameValueDto:用于na ...

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

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

随机推荐

  1. Semantic difference between object expressions and declarations

    object expressions are executed (and initialized) immediately, where they are used; object declarati ...

  2. 从 Python 快速启动 CGI 服务器

    很多人知道 Python 3 可以快速启动一个 HTTP 服务器: $ python3 -m http.server 8000 今天我查阅 http.server 模块发现它支持运行 CGI 脚本,只 ...

  3. git 仓库中删除历史大文件

    git 仓库中删除历史大文件 在git中增加了一个很大的文件,而且被保存在历史提交记录中,每次拉取代码都很大,速度很慢.而且用删除 提交历史记录的方式不是很实际. 以下分几个步骤介绍如何减小.git文 ...

  4. pwn学习之一

    刚刚开始学习pwn,记录一下自己学习的过程. 今天完成了第一道pwn题目的解答,做的题目是2017年TSCTF的bad egg,通过这道题学习到了一种getshell的方法:通过在大小不够存储shel ...

  5. 使用Tornado异步接入第三方(支付宝)支付

    目前国内比较流行的第三方支付主要有支付宝和微信支付,博主最近研究了下如何用Python接入支付宝支付,这里我以Tornado作为web框架,接入支付宝构造支付接口. 使用Tornado异步接入支付宝支 ...

  6. event、fly.js、购物车特效

    先总结下区别: #鼠标相对于浏览器窗口可视区域的X,Y坐标(窗口坐标),可视区域不包括工具栏和滚动条. event.clientX.event.clientY #鼠标相对于document文档区域的x ...

  7. Little Sub and Isomorphism Sequences ZOJ - 4089

    ZOJ - 4089 思路:可以反正 最长重构序列必然符合  此模式 x  +  {   }  与  {   }  +  x 那么 题意转化为了  找两个距离最长的相同的数.eeee 先离散化 然后 ...

  8. java简单框架设计

    设计框架包可以作为一个工具给大家用,需要有完全不同设计思路给出来,不同于我们去做一个web服务.网站. 或者一个业务微服务,需要从原来使用视角转换成一个构建者视角. 框架或者工具,更多是框架来管理或者 ...

  9. 线段树模板1 [Luogu P3372]

    代码+注释: #include <iostream> #include <cstdio> using namespace std; int n, q, flag, x, y, ...

  10. centos7安装tomcat8.5

    1.下载 tomcat Linux 版本 tomcat 官网下载地址:http://tomcat.apache.org/download-80.cgi 百度云盘链接:链接: https://pan.b ...