Abp 审计模块源码解读
Abp 审计模块源码解读
Abp 框架为我们自带了审计日志功能,审计日志可以方便地查看每次请求接口所耗的时间,能够帮助我们快速定位到某些性能有问题的接口。除此之外,审计日志信息还包含有每次调用接口时客户端请求的参数信息,客户端的 IP 与客户端使用的浏览器。有了这些数据之后,我们就可以很方便地复现接口产生 BUG 时的一些环境信息。
源码地址Abp版本:5.1.3
初探
我通过abp脚手架创建了一个Acme.BookStore项目在BookStoreWebModule类使用了
app.UseAuditing()拓展方法。
我们通过F12可以看到AbpApplicationBuilderExtensions中间件拓展类源码地址如下代码AbpAuditingMiddleware中间件。
public static IApplicationBuilder UseAuditing(this IApplicationBuilder app)
{
return app
.UseMiddleware<AbpAuditingMiddleware>();
}
我们继续查看AbpAuditingMiddleware中间件源码源码地址下面我把代码贴上来一一解释(先从小方法解释)
- 请求过滤(因为不是所以方法我们都需要记录,比如用户登录/用户支付)
// 判断当前请求路径是否需要过滤
private bool IsIgnoredUrl(HttpContext context)
{
// AspNetCoreAuditingOptions.IgnoredUrls是abp维护了一个过滤URL的一个容器
return context.Request.Path.Value != null &&
AspNetCoreAuditingOptions.IgnoredUrls.Any(x => context.Request.Path.Value.StartsWith(x));
}
- 是否保存审计日志
private bool ShouldWriteAuditLog(HttpContext httpContext, bool hasError)
{
// 是否记录报错的审计日志
if (AuditingOptions.AlwaysLogOnException && hasError)
{
return true;
}
// 是否记录未登录产生的审计日志
if (!AuditingOptions.IsEnabledForAnonymousUsers && !CurrentUser.IsAuthenticated)
{
return false;
}
// 是否记录get请求产生的审计日志
if (!AuditingOptions.IsEnabledForGetRequests &&
string.Equals(httpContext.Request.Method, HttpMethods.Get, StringComparison.OrdinalIgnoreCase))
{
return false;
}
return true;
}
- 执行审计模块中间件
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// 判断审计模块是否开启,IsIgnoredUrl就是我们上面说的私有方法了。
if (!AuditingOptions.IsEnabled || IsIgnoredUrl(context))
{
await next(context);
return;
}
// 是否出现报错
var hasError = false;
// 审计模块管理
using (var saveHandle = _auditingManager.BeginScope())
{
Debug.Assert(_auditingManager.Current != null);
try
{
await next(context);
// 审计模块是否有记录错误到日志
if (_auditingManager.Current.Log.Exceptions.Any())
{
hasError = true;
}
}
catch (Exception ex)
{
hasError = true;
// 判断当前错误信息是否已经记录了
if (!_auditingManager.Current.Log.Exceptions.Contains(ex))
{
_auditingManager.Current.Log.Exceptions.Add(ex);
}
throw;
}
finally
{
// 判断是否记录
if (ShouldWriteAuditLog(context, hasError))
{
// 判断是否有工作单元(这里主要就是防止因为记录日志信息报错了,会影响主要的业务流程)
if (UnitOfWorkManager.Current != null)
{
await UnitOfWorkManager.Current.SaveChangesAsync();
}
// 执行保存
await saveHandle.SaveAsync();
}
}
}
}
上面我们主要梳理了审计模块的中间件逻辑,到这里我们对审计日志的配置会有一些印象了,AuditingOptions我们需要着重的注意,因为关系到审计模块一些使用细节。(这里我说说我的看法不管是在学习Abp的那一个模块,我们都需要知道对于的配置类中,每个属性的作用以及使用场景。)
深入
我们前面了解到审计模块的使用方式,为了了解其中的原理我们需要查看源码
Volo.Abp.Auditing类库源码地址。
AbpAuditingOptions配置类
public class AbpAuditingOptions
{
/// <summary>
/// 隐藏错误,默认值:true (没有看到使用)
/// </summary>
public bool HideErrors { get; set; }
/// <summary>
/// 启用审计模块,默认值:true
/// </summary>
public bool IsEnabled { get; set; }
/// <summary>
/// 审计日志的应用程序名称,默认值:null
/// </summary>
public string ApplicationName { get; set; }
/// <summary>
/// 是否为匿名请求记录审计日志,默认值:true
/// </summary>
public bool IsEnabledForAnonymousUsers { get; set; }
/// <summary>
/// 记录所以报错,默认值:true(在上面中间件代码有用到)
/// </summary>
public bool AlwaysLogOnException { get; set; }
/// <summary>
/// 审计日志功能的协作者集合,默认添加了 AspNetCoreAuditLogContributor 实现。
/// </summary>
public List<AuditLogContributor> Contributors { get; }
/// <summary>
/// 默认的忽略类型,主要在序列化时使用。
/// </summary>
public List<Type> IgnoredTypes { get; }
/// <summary>
/// 实体类型选择器。上下文中SaveChangesAsync有使用到
/// </summary>
public IEntityHistorySelectorList EntityHistorySelectors { get; }
/// <summary>
/// Get请求是否启用,默认值:false
/// </summary>
public bool IsEnabledForGetRequests { get; set; }
public AbpAuditingOptions()
{
IsEnabled = true;
IsEnabledForAnonymousUsers = true;
HideErrors = true;
AlwaysLogOnException = true;
Contributors = new List<AuditLogContributor>();
IgnoredTypes = new List<Type>
{
typeof(Stream),
typeof(Expression)
};
EntityHistorySelectors = new EntityHistorySelectorList();
}
}
AbpAuditingModule模块入口
下面代码即在组件注册的时候,会调用 AuditingInterceptorRegistrar.RegisterIfNeeded 方法来判定是否为实现类型(ImplementationType) 注入审计日志拦截器。
public class AbpAuditingModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.OnRegistred(AuditingInterceptorRegistrar.RegisterIfNeeded);
}
}
这里主要是通过 AuditedAttribute 、IAuditingEnabled、DisableAuditingAttribute来判断是否进行审计操作,前两个作用是,只要类型标注了 AuditedAttribute 特性,或者是实现了 IAuditingEnable 接口,都会为该类型注入审计日志拦截器。
而 DisableAuditingAttribute 类型则相反,只要类型上标注了该特性,就不会启用审计日志拦截器。某些接口需要 提升性能 的话,可以尝试使用该特性禁用掉审计日志功能。
public static class AuditingInterceptorRegistrar
{
public static void RegisterIfNeeded(IOnServiceRegistredContext context)
{
// 满足条件时,将会为该类型注入审计日志拦截器。
if (ShouldIntercept(context.ImplementationType))
{
context.Interceptors.TryAdd<AuditingInterceptor>();
}
}
private static bool ShouldIntercept(Type type)
{
// 是否忽略该类型
if (DynamicProxyIgnoreTypes.Contains(type))
{
return false;
}
// 是否启用审计
if (ShouldAuditTypeByDefaultOrNull(type) == true)
{
return true;
}
// 该类型是否存在方法使用了AuditedAttribut特性
if (type.GetMethods().Any(m => m.IsDefined(typeof(AuditedAttribute), true)))
{
return true;
}
return false;
}
public static bool? ShouldAuditTypeByDefaultOrNull(Type type)
{
// 启用审计特性
if (type.IsDefined(typeof(AuditedAttribute), true))
{
return true;
}
// 禁用审计特性
if (type.IsDefined(typeof(DisableAuditingAttribute), true))
{
return false;
}
// 审计接口
if (typeof(IAuditingEnabled).IsAssignableFrom(type))
{
return true;
}
return null;
}
}
AuditingManager审计管理
上面我们讲了审计模块中间件,审计模块配置,以及特殊过滤配置,接下来我们就要继续深入到实现细节部分,前面中间件
AuditingManager.BeginScope()代码是我们的入口,那就从这里开始下手源码地址。
从下面的代码我们可以知道其实就是创建一个DisposableSaveHandle代理类。(我们需要注意构造参数的值)
- 第一个this主要是将当前对象传入方法中
- 第二个
ambientScope重点是_auditingHelper.CreateAuditLogInfo()创建AuditLogInfo类(对应Current.log) - 第三个
Current.log当前AuditLogInfo信息 - 第四个
Stopwatch.StartNew()计时器
public IAuditLogSaveHandle BeginScope()
{
// 创建AuditLogInfo类复制到Current.Log中(其实是维护了一个内部的字典)
var ambientScope = _ambientScopeProvider.BeginScope(
AmbientContextKey,
new AuditLogScope(_auditingHelper.CreateAuditLogInfo())
);
return new DisposableSaveHandle(this, ambientScope, Current.Log, Stopwatch.StartNew());
}
_auditingHelper.CreateAuditLogInfo()从http请求上下文中获取,当前的url/请求参数/请求浏览器/ip.....
// 从http请求上下文中获取,当前的url/请求参数/请求浏览器/ip.....
public virtual AuditLogInfo CreateAuditLogInfo()
{
var auditInfo = new AuditLogInfo
{
ApplicationName = Options.ApplicationName,
TenantId = CurrentTenant.Id,
TenantName = CurrentTenant.Name,
UserId = CurrentUser.Id,
UserName = CurrentUser.UserName,
ClientId = CurrentClient.Id,
CorrelationId = CorrelationIdProvider.Get(),
ExecutionTime = Clock.Now,
ImpersonatorUserId = CurrentUser.FindImpersonatorUserId(),
ImpersonatorUserName = CurrentUser.FindImpersonatorUserName(),
ImpersonatorTenantId = CurrentUser.FindImpersonatorTenantId(),
ImpersonatorTenantName = CurrentUser.FindImpersonatorTenantName(),
};
ExecutePreContributors(auditInfo);
return auditInfo;
}
DisposableSaveHandle代理类中提供了一个SaveAsync()方法,调用AuditingManager.SaveAsync()当然这个SaveAsync()方法大家还是有一点点印象的吧,毕竟中间件最后完成之后就会调用该方法。
protected class DisposableSaveHandle : IAuditLogSaveHandle
{
public AuditLogInfo AuditLog { get; }
public Stopwatch StopWatch { get; }
private readonly AuditingManager _auditingManager;
private readonly IDisposable _scope;
public DisposableSaveHandle(
AuditingManager auditingManager,
IDisposable scope,
AuditLogInfo auditLog,
Stopwatch stopWatch)
{
_auditingManager = auditingManager;
_scope = scope;
AuditLog = auditLog;
StopWatch = stopWatch;
}
// 包装AuditingManager.SaveAsync方法
public async Task SaveAsync()
{
await _auditingManager.SaveAsync(this);
}
public void Dispose()
{
_scope.Dispose();
}
}
AuditingManager.SaveAsync()主要做的事情也主要是组建AuditLogInfo信息,然后调用SimpleLogAuditingStore.SaveAsync(),SimpleLogAuditingStore 实现,其内部就是调用 ILogger 将信息输出。如果需要将审计日志持久化到数据库,你可以实现 IAUditingStore 接口,覆盖原有实现 ,或者使用 ABP vNext 提供的 Volo.Abp.AuditLogging 模块。
protected virtual async Task SaveAsync(DisposableSaveHandle saveHandle)
{
// 获取审计记录
BeforeSave(saveHandle);
// 调用AuditingStore.SaveAsync
await _auditingStore.SaveAsync(saveHandle.AuditLog);
}
// 获取审计记录
protected virtual void BeforeSave(DisposableSaveHandle saveHandle)
{
saveHandle.StopWatch.Stop();
saveHandle.AuditLog.ExecutionDuration = Convert.ToInt32(saveHandle.StopWatch.Elapsed.TotalMilliseconds);
// 获取请求返回Response.StatusCode
ExecutePostContributors(saveHandle.AuditLog);
// 获取实体变化
MergeEntityChanges(saveHandle.AuditLog);
}
// 获取请求返回Response.StatusCode
protected virtual void ExecutePostContributors(AuditLogInfo auditLogInfo)
{
using (var scope = ServiceProvider.CreateScope())
{
var context = new AuditLogContributionContext(scope.ServiceProvider, auditLogInfo);
foreach (var contributor in Options.Contributors)
{
try
{
contributor.PostContribute(context);
}
catch (Exception ex)
{
Logger.LogException(ex, LogLevel.Warning);
}
}
}
}
总结
首先审计模块的一些设计思路YYDS,审计模块的作用显而易见,但是在使用过程中注意利弊,好处就是方便我们进行错误排除,实时监控系统的健康。但是同时也会导致我们接口变慢(毕竟要记录日志信息),当然还要提到一点就是我们在阅读源码的过程中先了解模块是做什么的,然后了解基础的配置信息,再然后就是通过代码入口一层一层剖析就好了。
Abp 审计模块源码解读的更多相关文章
- 分布式事务中间件 Fescar—RM 模块源码解读
前言 在SOA.微服务架构流行的年代,许多复杂业务上需要支持多资源占用场景,而在分布式系统中因为某个资源不足而导致其它资源占用回滚的系统设计一直是个难点.我所在的团队也遇到了这个问题,为解决这个问题上 ...
- koa2--delegates模块源码解读
delegates模块是由TJ大神写的,该模块的作用是将内部对象上的变量或函数委托到外部对象上.然后我们就可以使用外部对象就能获取内部对象上的变量或函数.delegates委托方式有如下: gette ...
- Webpack探索【16】--- 懒加载构建原理详解(模块如何被组建&如何加载)&源码解读
本文主要说明Webpack懒加载构建和加载的原理,对构建后的源码进行分析. 一 说明 本文以一个简单的示例,通过对构建好的bundle.js源码进行分析,说明Webpack懒加载构建原理. 本文使用的 ...
- Webpack探索【15】--- 基础构建原理详解(模块如何被组建&如何加载)&源码解读
本文主要说明Webpack模块构建和加载的原理,对构建后的源码进行分析. 一 说明 本文以一个简单的示例,通过对构建好的bundle.js源码进行分析,说明Webpack的基础构建原理. 本文使用的W ...
- AFNetworking 3.0 源码解读(一)之 AFNetworkReachabilityManager
做ios开发,AFNetworking 这个网络框架肯定都非常熟悉,也许我们平时只使用了它的部分功能,而且我们对它的实现原理并不是很清楚,就好像总是有一团迷雾在眼前一样. 接下来我们就非常详细的来读一 ...
- AfNetworking 3.0源码解读
做ios开发,AFNetworking 这个网络框架肯定都非常熟悉,也许我们平时只使用了它的部分功能,而且我们对它的实现原理并不是很清楚,就好像总是有一团迷雾在眼前一样. 接下来我们就非常详细的来读一 ...
- seajs 源码解读
之前面试时老问一个问题seajs 是怎么加载js 文件的 在网上找一些资料,觉得这个写的不错就转载了,记录一下,也学习一下 seajs 源码解读 seajs 简单介绍 seajs是前端应用模块化开发的 ...
- Normalize.css 介绍与源码解读
开始 Normalize.css 是一个可定制的 CSS 文件,使浏览器呈现的所有元素,更一致和符合现代标准;是在现代浏览器环境下对于CSS reset的替代. 它正是针对只需要统一的元素样式.该项目 ...
- SDWebImage源码解读之SDWebImagePrefetcher
> 第十篇 ## 前言 我们先看看`SDWebImage`主文件的组成模块: 
题目描述 在河上有一座独木桥,一只青蛙想沿着独木桥从河的一侧跳到另一侧.在桥上有一些石子,青蛙很讨厌踩在这些石子上.由于桥的长度和青蛙一次跳过的距离都是正整数,我们可以把独木桥上青蛙可能到达的点看成数 ...
- 常见分布式唯一ID生成策略
方法一: 用数据库的 auto_increment 来生成 优点: 此方法使用数据库原有的功能,所以相对简单 能够保证唯一性 能够保证递增性 id 之间的步长是固定且可自定义的 缺点: 可用性难以保证 ...
- 第四十七个知识点:什么是Fiat-Shamir变换?
第四十七个知识点:什么是Fiat-Shamir变换? 只要Alice和Bob同时在线,Sigma协议能快速的完成Alice向Bob证明的任务.Alice向Bob发送承诺,Bob返回一个挑战,最后Ali ...
- Masked Gradient-Based Causal Structure Learning
目录 概 主要内容 最终的目标 代码 Ng I., Fang Z., Zhu S., Chen Z. and Wang J. Masked Gradient-Based Causal Structur ...
- ADAM : A METHOD FOR STOCHASTIC OPTIMIZATION
目录 概 主要内容 算法 选择合适的参数 一些别的优化算法 AdaMax 理论 代码 Kingma D P, Ba J. Adam: A Method for Stochastic Optimizat ...
- 编写Java程序,使用JDBC连接SQL Server数据库
返回本章节 返回作业目录 需求说明: 使用JDBC连接SQL Server数据库 SQL Server数据库位于192.168.2.101. 所需连接的数据库为eshop_db,用户名为test,密码 ...
- 对接网易云信音视频2.0呼叫组件集成到vue中,实现web端呼叫app,视频语音通话。
项目中需要实现视频通话功能,经过公司的赛选,采用网易云信的视频通话服务,app小伙伴集成很顺利.web端需要实现呼叫app端用户.网易云信文档介绍不全,vue的demo满足不了需求,和客服人员沟通,只 ...
- mt19937
额,这个是一个小记.没什么,就是记给自己看的,你可以走了. mt19937 需要 C++11.生成高质量随机数. mt19937 rnd(chrono::system_clock::now().tim ...
- vsconde launch.json配置 调试本地文件
{ // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing ...
- css 文本基础 实战 小米官方卡片案例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...