需求背景

最近在项目上需要增加对用户操作进行审计日志记录的功能,调研了一圈,在.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的更多相关文章

  1. .NET Core工程应用系列(2) 实现可配置Attribute的Json序列化方案

    背景 在这篇文章中,我们实现了基于自定义Attribute的审计日志数据对象属性过滤,但是在实际项目的应用中遇到了一点麻烦.需要进行审计的对象属性中会包含其他类对象,而我们之前的实现是没办法处理这种类 ...

  2. ASP.NET Core应用的错误处理[3]:ExceptionHandlerMiddleware中间件如何呈现“定制化错误页面”

    DeveloperExceptionPageMiddleware中间件利用呈现出来的错误页面实现抛出异常和当前请求的详细信息以辅助开发人员更好地进行纠错诊断工作,而ExceptionHandlerMi ...

  3. kettle系列-4.kettle定制化开发工具类

    要说的话这个工具类还是比较简单的,每个方法体都比较小,但用起来还是可以的,把开发中一些常用的步骤封装了下,不用去kettle源码中找相关操作的具体实现了. 算了废话不多了,直接上重点,代码如下: im ...

  4. jquery-ui-datepicker定制化,汉化,因手机布局美观化源码修改

    感谢浏览,欢迎交流=.= 公司微信网页需要使用日历控件,想到jquery-mobile,但是css影响页面布局,放弃后使用jquery-ui-datepicker. 话不多说,进入正题: 1.jque ...

  5. 定制化Azure站点Java运行环境(3)

    定制化Azure Website提供的默认的Tomcat和JDK环境 在我们之前的测试中,如果你访问你的WEB站点URL时不加任何上下文,实际上你看到的web界面是系统自带的测试页面index.jsp ...

  6. 使用beanstalkd实现定制化持续集成过程中pipeline

    持续集成是一种项目管理和流程模型,依赖于团队中各个角色的配合.各个角色的意识和配合不是一朝一夕能练就的,我们的工作只是提供一种方案和能力,这就是持续集成能力的服务化.而在做持续集成能力服务化的过程中, ...

  7. .net core实践系列之短信服务-Api的SDK的实现与测试

    前言 上一篇<.net core实践系列之短信服务-Sikiro.SMS.Api服务的实现>讲解了API的设计与实现,本篇主要讲解编写接口的SDK编写还有API的测试. 或许有些人会认为, ...

  8. U-Mail:如何实现EDM的个性化和定制化?

    设想一下,一个上班族一天要接到多少垃圾邮件?据媒体报道,目前来往的邮件中,高达95%以上的是垃圾邮件,而且有些垃圾邮件还会故意占据着邮箱的最前列.同时,随着人们接受资讯越来越快捷便利,渠道越来越多,也 ...

  9. ElasticSearch 2 (13) - 深入搜索系列之结构化搜索

    ElasticSearch 2 (13) - 深入搜索系列之结构化搜索 摘要 结构化查询指的是查询那些具有内在结构的数据,比如日期.时间.数字都是结构化的.它们都有精确的格式,我们可以对这些数据进行逻 ...

随机推荐

  1. buu misc 1-32 wp

    buuCTFwp(1~32) 1.签到题 题里就有flag flag{buu_ctf} 2.二维码 1.题目是一个二维码,用010发现提示四位数字,想到应该是暗藏压缩包 2.虚拟机foremost分离 ...

  2. vue项目中使用canvas

    canvas API 文档:https://www.canvasapi.cn/ 一.在html中使用canvas canvas 元素用于在网页上绘制图形.  在html中,使用 document.ge ...

  3. LeeCode刷题笔记

    (本来想在LeeCode题目页面上做注释的,结果没找到位置,只好来这里了) 字符串部分: 14.最长公共前缀:编写一个函数来查找字符串数组中的最长公共前缀. 示例 1: 输入: ["flow ...

  4. 【基因组注释】ncRNA注释

    目录 1. ncRNA 2. 软件 tRNA注释 rRNA注释 其他ncRNA注释 3. 注释 tRNA rRNA snRNA.miRNA等 4. snRNA.miRNA等结果的统计 1. ncRNA ...

  5. python-django-自定义查询Q函数和F函数

    数据库: def page_q(request): """Q函数的使用""" #查询username和nickname都是zhangsan ...

  6. R 多图间距调整

    在R中多图画到一起的时候,各图间距通常默认的较远. 如下图: 1 par(mfcol=c(2,1)) 2 plot(1:100) 3 plot(1:100) 调整图片间距这时我们要用到par()函数中 ...

  7. A Child's History of England.5

    Above all, it was in the Roman time, and by means of Roman ships, that the Christian Religion was fi ...

  8. oracle 执行计划的获取方法

    1.用explain plan for来获取执行计划 explain plan for <sql>; select * from table(dbms_xplan.display()); ...

  9. MySQL 迁移到 Redis 记

    前些日子,一个悠闲又不悠闲的下午,我还在用 Node.js 写着某个移动互联网应用的 API 服务端.那时还是用 MySQL 作为数据库,一切都很好,所有功能正常运行.可是有很多问题让人不安: 频繁的 ...

  10. vue引入d3

    单页面使用 cnpm install d3 --save-dev 指定版本安装 cnpm install d3@6.3.1 -S <script> import * as d3 from ...