.NET Core工程应用系列(1) 定制化Audit.NET实现自定义AuditTarget
需求背景
最近在项目上需要增加对用户操作进行审计日志记录的功能,调研了一圈,在.net core生态里,用的最多的是Audit.NET。浏览完这个库的文档后,觉得大致能满足我们的诉求,于是建立一个控制台项目来先玩一玩。
但是我们还有额外的需求:
- 我们要记录的数据中包含了一些用户的敏感信息,这些内容是肯定不能记到审计日志里面的,所以得想个办法在写日志的时候把这些内容给去掉,这是这篇文章要解决的问题。
- 我们需要将日志数据发送到AWS的Simple Queue Service里面去,但是官方提供的一些预定义的
DataProvider里没有这个功能,需要自己实现,这部分就不在这篇文章里说了,下次再写一篇。
Audit.NET的基本使用方法
安装
新建一个.net core console application,我的源代码在这里:TryCustomAuditNet, 使用Nuget查找Audit.NET安装到项目中即可。
配置
这个库的官方文档已经有比较详细的配置项说明了,在这里我就不复述了,只记录一下基本配置。
static void Main(string[] args)
{
ConfigureAudit();
}
private static void ConfigureAudit()
{
Audit.Core.Configuration.Setup()
.UseFileLogProvider(config => config
.DirectoryBuilder(_ => "./")
.FilenameBuilder(auditEvent => $"{auditEvent.EventType}_{DateTime.Now.Ticks}.json"));
}
使用
定义数据对象
首先我们模拟一个需要被审计的数据对象Order,写一个方法用来修改其中一个属性:
public class Order
{
public Guid Id { get; set; }
public string CustomerName { get; set; }
public int TotalAmount { get; set; }
public DateTime OrderTime { get; set; }
public Order(Guid id, string customerName, int totalAmount, DateTime orderTime)
{
Id = id;
CustomerName = customerName;
TotalAmount = totalAmount;
OrderTime = orderTime;
}
public void UpdateOrderAmount(int newOrderAmount)
{
TotalAmount = newOrderAmount;
}
}
业务逻辑中进行审计
static void Main(string[] args)
{
ConfigureAudit();
var order = new Order(Guid.NewGuid(), "Jone Doe", 100, DateTime.UtcNow);
// 追踪order的审计
using (var scope = AuditScope.Create("Order::Update", () => order))
{
order.UpdateOrderAmount(200);
// optional
scope.Comment("this is a test for update order.");
}
}
效果
运行程序,在TryCustomAuditNet/bin/Debug/netcoreapp3.1目录下生成了一个审计日志文件Order::Update_637408091235053310.json

内容如下:
$ cat Order::Update_637408091235053310.json
{
"EventType": "Order::Update",
"Environment": {
"UserName": "yu.li1",
"MachineName": "Yus-MacBook-Pro",
"DomainName": "Yus-MacBook-Pro",
"CallingMethodName": "TryCustomAuditNet.Program.Main()",
"AssemblyName": "TryCustomAuditNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"Culture": ""
},
"Target": {
"Type": "Order",
"Old": {
"Id": "411b43b5-24be-4368-8978-2bf334e7ce8e",
"CustomerName": "Jone Doe",
"TotalAmount": 100,
"OrderTime": "2020-11-12T12:18:43.177177Z"
},
"New": {
"Id": "411b43b5-24be-4368-8978-2bf334e7ce8e",
"CustomerName": "Jone Doe",
"TotalAmount": 200,
"OrderTime": "2020-11-12T12:18:43.177177Z"
}
},
"Comments": [
"this is a test for update order."
],
"StartDate": "2020-11-12T12:18:43.212662Z",
"EndDate": "2020-11-12T12:18:43.498007Z",
"Duration": 285
}
可以看到在审计过程中,数据对象的TotalAmount值从100更新为了200,并且新增了一个Comments字段。
问题
我们的问题是,在审计日志中,我们不希望记录CustomerName这个字段的值,因为具体的人名被认为是显式的隐私数据,而这是不能直接记录到审计日志中的,怎么处理?
解决方案
一个比较简单粗暴的方法就是在需要记录审计日志的地方,将原始的数据对象经过映射之后传到AuditScope内部,但是这有几个问题:一是这样一来需要在程序中写大量不同的数据对象映射方法,不利于维护;二是我没有实验这种方式的开销有多大以及到底能不能准确实现我们的需求。所以我们去看看源码,然后整理一下思路。
核心代码
打开Audit.NET的源代码,结合测试程序,我们定位到了几个关键的代码块:
AuditScope.cs
public partial class AuditScope : IAuditScope
{
private readonly AuditScopeOptions _options;
#region Constructors
[MethodImpl(MethodImplOptions.NoInlining)]
internal AuditScope(AuditScopeOptions options)
{
_options = options;
_creationPolicy = options.CreationPolicy ?? Configuration.CreationPolicy;
_dataProvider = options.DataProvider ?? Configuration.DataProvider;
_targetGetter = options.TargetGetter;
// ... 省略中间代码
if (options.TargetGetter != null)
{
var targetValue = options.TargetGetter.Invoke();
_event.Target = new AuditTarget
{
// IMPORTANT: 调用了AuditDataProvider中的Serialize方法来序列化数据对象
Old = _dataProvider.Serialize(targetValue),
Type = targetValue?.GetType().GetFullTypeName() ?? "Object"
};
}
ProcessExtraFields(options.ExtraFields);
}
// ...省略其他代码
}
AuditDataProvider.cs
// IMPORTANT: AuditDataProvider这是一个抽象基类,我们可以通过继承AuditDataProvider实现自己的DataProvider。
public virtual object Serialize<T>(T value)
{
// IMPORTANT:重写这个方法,在重写中实现基于Attribute的数据对象字段过滤。
if (value == null)
{
return null;
}
return JToken.FromObject(value, JsonSerializer.Create(Configuration.JsonSettings));
}
基本思路
基本思路就是我们设法在需要记录的数据对象定义里,给需要或者不需要记录的属性加上自定义的Attribute,并且实现自己的DataProvider类重写Serialize方法,在序列化对象的时候根据这个特定的Attribute来过滤需要序列化的字段。
那么就搞起来。
代码实现
添加自定义Attribue
新建类UnAuditableAttribute,实现代码:
[AttributeUsage(AttributeTargets.Property)]
public class UnAuditableAttribute: Attribute
{
}
为我们不希望被审计的属性添加Attribute:
public class Order
{
public Guid Id { get; set; }
[UnAuditable]
public string CustomerName { get; set; }
// ...省略其他内容
}
添加自定义DataProvider并重写关键方法
为了简单,我们直接复制一份FileDataProvider类的内容到我们新建的CustomFileDataProvider类中:
public class CustomFileDataProvider: AuditDataProvider
{
public override object Serialize<T>(T value)
{
if (value == null)
{
return null;
}
// REGION START: 过滤属性
var jo = new JObject();
var serializer = JsonSerializer.Create(Configuration.JsonSettings);
foreach (PropertyInfo propInfo in value.GetType().GetProperties())
{
if (propInfo.CanRead)
{
object propVal = propInfo.GetValue(value, null);
var cutomAttribute = propInfo.GetCustomAttribute<UnAuditableAttribute>();
if (cutomAttribute == null)
{
// 被打上UnAuditableAttribute标记的属性,不加入序列化中。
jo.Add(propInfo.Name, JToken.FromObject(propVal, serializer));
}
}
}
// REGION END
return JToken.FromObject(jo, serializer);
}
public CustomFileDataProvider(Action<IFileLogProviderConfigurator> config)
{
// 为了在我们的测试工程中编译通过,需要自定义一个CustomFileDataProviderConfigurator类,实现照搬FileDataProviderConfigurator,只是将字段改为public的。
var fileConfig = new CustomFileDataProviderConfigurator();
if (config != null)
{
config.Invoke(fileConfig);
_directoryPath = fileConfig._directoryPath;
_directoryPathBuilder = fileConfig._directoryPathBuilder;
_filenameBuilder = fileConfig._filenameBuilder;
_filenamePrefix = fileConfig._filenamePrefix;
JsonSettings = fileConfig._jsonSettings;
}
}
// ...省略其他相同的内容
}
修改配置
最后我们修改一下最初的配置,使用我们自定义的DataProvider:
private static void ConfigureAudit()
{
Audit.Core.Configuration.Setup()
.UseCustomProvider(new CustomFileDataProvider(config => config
.DirectoryBuilder(_ => "./")
.FilenameBuilder(auditEvent => $"{auditEvent.EventType}_{DateTime.Now.Ticks}.json")
.JsonSettings(new JsonSerializerSettings
{
Formatting = Formatting.Indented,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
NullValueHandling = NullValueHandling.Include
})));
}
测试
再运行一次程序,我们来看生成的审计文件:
$ cat Order::Update_637408110805063180.json
{
"EventType": "Order::Update",
"Environment": {
"UserName": "yu.li1",
"MachineName": "Yus-MacBook-Pro",
"DomainName": "Yus-MacBook-Pro",
"CallingMethodName": "TryCustomAuditNet.Program.Main()",
"AssemblyName": "TryCustomAuditNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"Culture": ""
},
"Target": {
"Type": "Order",
"Old": {
"Id": "f5c08f91-c0e4-4c25-b5ba-97edfb418346",
"TotalAmount": 100,
"OrderTime": "2020-11-12T12:51:19.081762Z"
},
"New": {
"Id": "f5c08f91-c0e4-4c25-b5ba-97edfb418346",
"TotalAmount": 200,
"OrderTime": "2020-11-12T12:51:19.081762Z"
}
},
"Comments": [
"this is a test for update order."
],
"StartDate": "2020-11-12T12:51:19.215596Z",
"EndDate": "2020-11-12T12:51:20.472729Z",
"Duration": 1257
}
注意到新的审计日志中已经不再包含CustomerName这个属性了,完美。
总结
Audit.NET这个框架还是非常强大的,这篇文章只探讨了其中非常小的一个特性点,当然基于本文的思路,我们还可以实现更复杂的审计日志数据对象过滤逻辑,可扩展性还是很好的。
.NET Core工程应用系列(1) 定制化Audit.NET实现自定义AuditTarget的更多相关文章
- .NET Core工程应用系列(2) 实现可配置Attribute的Json序列化方案
背景 在这篇文章中,我们实现了基于自定义Attribute的审计日志数据对象属性过滤,但是在实际项目的应用中遇到了一点麻烦.需要进行审计的对象属性中会包含其他类对象,而我们之前的实现是没办法处理这种类 ...
- ASP.NET Core应用的错误处理[3]:ExceptionHandlerMiddleware中间件如何呈现“定制化错误页面”
DeveloperExceptionPageMiddleware中间件利用呈现出来的错误页面实现抛出异常和当前请求的详细信息以辅助开发人员更好地进行纠错诊断工作,而ExceptionHandlerMi ...
- kettle系列-4.kettle定制化开发工具类
要说的话这个工具类还是比较简单的,每个方法体都比较小,但用起来还是可以的,把开发中一些常用的步骤封装了下,不用去kettle源码中找相关操作的具体实现了. 算了废话不多了,直接上重点,代码如下: im ...
- jquery-ui-datepicker定制化,汉化,因手机布局美观化源码修改
感谢浏览,欢迎交流=.= 公司微信网页需要使用日历控件,想到jquery-mobile,但是css影响页面布局,放弃后使用jquery-ui-datepicker. 话不多说,进入正题: 1.jque ...
- 定制化Azure站点Java运行环境(3)
定制化Azure Website提供的默认的Tomcat和JDK环境 在我们之前的测试中,如果你访问你的WEB站点URL时不加任何上下文,实际上你看到的web界面是系统自带的测试页面index.jsp ...
- 使用beanstalkd实现定制化持续集成过程中pipeline
持续集成是一种项目管理和流程模型,依赖于团队中各个角色的配合.各个角色的意识和配合不是一朝一夕能练就的,我们的工作只是提供一种方案和能力,这就是持续集成能力的服务化.而在做持续集成能力服务化的过程中, ...
- .net core实践系列之短信服务-Api的SDK的实现与测试
前言 上一篇<.net core实践系列之短信服务-Sikiro.SMS.Api服务的实现>讲解了API的设计与实现,本篇主要讲解编写接口的SDK编写还有API的测试. 或许有些人会认为, ...
- U-Mail:如何实现EDM的个性化和定制化?
设想一下,一个上班族一天要接到多少垃圾邮件?据媒体报道,目前来往的邮件中,高达95%以上的是垃圾邮件,而且有些垃圾邮件还会故意占据着邮箱的最前列.同时,随着人们接受资讯越来越快捷便利,渠道越来越多,也 ...
- ElasticSearch 2 (13) - 深入搜索系列之结构化搜索
ElasticSearch 2 (13) - 深入搜索系列之结构化搜索 摘要 结构化查询指的是查询那些具有内在结构的数据,比如日期.时间.数字都是结构化的.它们都有精确的格式,我们可以对这些数据进行逻 ...
随机推荐
- c链表中指针的一些用法要点
/* 结构体不能含有同类型的结构,但是可以含有指向同类型结构的指针.这样的定义是定义一个链表的基础. */1 typedef int Element; 2 3 typedef struct node{ ...
- 洛谷 P3711 - 仓鼠的数学题(多项式)
洛谷题面传送门 提供一种不太一样的做法. 假设要求的多项式为 \(f(x)\).我们考察 \(f(x)-f(x-1)\),不难发现其等于 \(\sum\limits_{i=0}^na_ix^i\) 考 ...
- 洛谷 P6222 - 「P6156 简单题」加强版(莫比乌斯反演)
原版传送门 & 加强版传送门 题意: \(T\) 组数据,求 \(\sum\limits_{i=1}^n\sum\limits_{j=1}^n(i+j)^k\mu^2(\gcd(i,j))\g ...
- P4550 收集邮票 与 灵异的期望
考前复习一下期望相关知识,这题的期望还是很巧妙的. 设 \(f_{i}\) 表示已经买到了 \(i\) 张不同的邮票的期望步数,\(g_{i}\) 表示表示已经买到了 \(i\) 张不同的邮票的期望花 ...
- Python使用print打印时,展示内容不换行
原理 Python的print()函数中参数end='' 默认为\n,所以会自动换行; 默认的print()函数: print(end='\n') 方案 Python 2: 在print语句的末尾加上 ...
- PHP-FPM运行状态的实时查看及监控详解
https://www.jb51.net/article/97640.htm https://blog.csdn.net/Dr_cokiy/article/details/105580758
- 25-ZigZag Conversion
The string "PAYPALISHIRING" is written in a zigzag pattern on a given number of rows like ...
- 用usb线配置直流电机驱动器不能配置成功
原因可能是因为usb线的问题 换了三条usb线. 这三条都是通的,用万用表测试都是通的,但是进行电机配置的时候不行. 猜测原因可能是三条usb线的芯材质不同导致压降不同,使得通信故障.
- Kotlin 学习(1)
本文出自链接:https://www.jianshu.com/p/ef9584a8ebf8 Kotlin的插件安装: Settings->Plugins->Browse Repositor ...
- spring认证的一些核心类
SecurityContextHolder, to provide access to the SecurityContext. SecurityContext: to hold the Authen ...