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

这里主要是通过 AuditedAttributeIAuditingEnabledDisableAuditingAttribute来判断是否进行审计操作,前两个作用是,只要类型标注了 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 审计模块源码解读的更多相关文章

  1. 分布式事务中间件 Fescar—RM 模块源码解读

    前言 在SOA.微服务架构流行的年代,许多复杂业务上需要支持多资源占用场景,而在分布式系统中因为某个资源不足而导致其它资源占用回滚的系统设计一直是个难点.我所在的团队也遇到了这个问题,为解决这个问题上 ...

  2. koa2--delegates模块源码解读

    delegates模块是由TJ大神写的,该模块的作用是将内部对象上的变量或函数委托到外部对象上.然后我们就可以使用外部对象就能获取内部对象上的变量或函数.delegates委托方式有如下: gette ...

  3. Webpack探索【16】--- 懒加载构建原理详解(模块如何被组建&如何加载)&源码解读

    本文主要说明Webpack懒加载构建和加载的原理,对构建后的源码进行分析. 一 说明 本文以一个简单的示例,通过对构建好的bundle.js源码进行分析,说明Webpack懒加载构建原理. 本文使用的 ...

  4. Webpack探索【15】--- 基础构建原理详解(模块如何被组建&如何加载)&源码解读

    本文主要说明Webpack模块构建和加载的原理,对构建后的源码进行分析. 一 说明 本文以一个简单的示例,通过对构建好的bundle.js源码进行分析,说明Webpack的基础构建原理. 本文使用的W ...

  5. AFNetworking 3.0 源码解读(一)之 AFNetworkReachabilityManager

    做ios开发,AFNetworking 这个网络框架肯定都非常熟悉,也许我们平时只使用了它的部分功能,而且我们对它的实现原理并不是很清楚,就好像总是有一团迷雾在眼前一样. 接下来我们就非常详细的来读一 ...

  6. AfNetworking 3.0源码解读

    做ios开发,AFNetworking 这个网络框架肯定都非常熟悉,也许我们平时只使用了它的部分功能,而且我们对它的实现原理并不是很清楚,就好像总是有一团迷雾在眼前一样. 接下来我们就非常详细的来读一 ...

  7. seajs 源码解读

    之前面试时老问一个问题seajs 是怎么加载js 文件的 在网上找一些资料,觉得这个写的不错就转载了,记录一下,也学习一下 seajs 源码解读 seajs 简单介绍 seajs是前端应用模块化开发的 ...

  8. Normalize.css 介绍与源码解读

    开始 Normalize.css 是一个可定制的 CSS 文件,使浏览器呈现的所有元素,更一致和符合现代标准;是在现代浏览器环境下对于CSS reset的替代. 它正是针对只需要统一的元素样式.该项目 ...

  9. SDWebImage源码解读之SDWebImagePrefetcher

    > 第十篇 ## 前言 我们先看看`SDWebImage`主文件的组成模块: ![](http://images2015.cnblogs.com/blog/637318/201701/63731 ...

随机推荐

  1. 【LeetCode】706. Design HashMap 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 日期 题目地址:https://leetcode.c ...

  2. 【剑指Offer】平衡二叉树 解题报告(Python & C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 解题方法 日期 题目地址:https://www.nowcoder.co ...

  3. 【LeetCode】849. Maximize Distance to Closest Person 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 日期 题目地址:https://leetcode.c ...

  4. 【Web前端】css属性cursor注意事项

    注意使用cursor的url时url的括号后面必须+   ,auto: 错误示范:cursor:url('../picture/head.cur'); 正确示范:cursor: url(". ...

  5. css怎么实现雪人

    冬天来了,怎么能少的了雪人呢,不管是现实中还是程序员的代码中统统都的安排上,那就一次安排几个雪人兄弟,咱们先看效果图: 有喜欢的就赶紧cv拿走吧!!! 其详细代码如下: 图1 html部分: < ...

  6. CS5213demoboard设计电路|DMI转VGA带II2S音频输出转接线|CS5213方案

    CS5213是台湾CAPSTONE瑞奇达推出的一款HDMI(高清多媒体接口)到VGA转换芯片. CS5213设计HDMI转VGA带II2S转接线产品特性: ◇将完整的HDMI信号转换为VGA输出◇支持 ...

  7. 【Redis的那些事 · 续集】Redis的位图、HyperLogLog数据结构演示以及布隆过滤器

    一.Redis位图 1.位图的最小单位是bit,每个bit的值只能是0和1,位图的应用场景一般用于一些签到记录,例如打卡等. 场景举例: 例如某APP要存储用户的打卡记录,如果按照正常的思路来做,可能 ...

  8. Spring企业级程序设计作业目录(作业笔记)

    Spring企业级程序设计 • [目录] 第1章 Spring之旅  >>> 1.1.6 使用Eclipse搭建的Spring开发环境,使用set注入方式为Bean对象注入属性值并打 ...

  9. .NET 云原生架构师训练营(设计原则&&设计模式)--学习笔记

    目录 设计原则 设计模式 设计原则 DRY (Don't repeat yourself 不要重复) KISS (Keep it stupid simple 简单到傻子都能看懂) YAGNI (You ...

  10. Python_time&datetime

    获取常用日常时间 # encoding: utf-8 import time import datetime # 当前时间 datetime_now_time = datetime.datetime. ...