需求背景

最近在项目上需要增加对用户操作进行审计日志记录的功能,调研了一圈,在.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. [cf1184E]Daleks' Invasion

    先求出任意一棵最小生成树,然后对边分类讨论1.非树边,答案即最小生成树的环上的最长边2.树边,反过来考虑,相当于对于每一个点对那条路经打上标记,取min对于1直接用倍增维护即可,对于2可以用树链剖分/ ...

  2. [bzoj1106]立方体大作战

    先贪心,容易发现如果两个点中间没有点对,那么一定可以先把这两个点消掉分析一下,就可以发现这样两个点的答案就是这两个点对中间不成对的点数量扫描过去,线段树维护每一个点的权值(是否会被算入答案)即可 1 ...

  3. 明明pip安装python的模块了,pycharm还是找不到的解决方案

    以前pycharm的安装包和python的环境一直都不能融合在一起,到了今天才知道,原来他们都是有自己的工作环境的 自己的工作环境(虚拟解释器)和安装python的工作环境(基本解释器)不是一个环境, ...

  4. Revit二次开发之获取本机已安装的Revit版本与路径

    在revit安装目录下找到 RevitAddInUtility.dll 在项目中引用 使用以下方法 using Autodesk.RevitAddIns; /// <summary> // ...

  5. LOJ 2353 & 洛谷 P4027 [NOI2007]货币兑换(CDQ 分治维护斜率优化)

    题目传送门 纪念一下第一道(?)自己 yy 出来的 NOI 题. 考虑 dp,\(dp[i]\) 表示到第 \(i\) 天最多有多少钱. 那么有 \(dp[i]=\max\{\max\limits_{ ...

  6. 洛谷 P3994 高速公路(斜率优化)

    题目链接 题意:给出一棵树,\(1\) 号点为根,边上有边权. 每个点有两个参数 \(p_i,q_i\) 如果你想从 \(i\) 号点到与其距离为 \(d\) 的 \(j\) 号点,那么你需花费 \( ...

  7. Codeforces 1276F - Asterisk Substrings(SAM+线段树合并+虚树)

    Codeforces 题面传送门 & 洛谷题面传送门 SAM hot tea %%%%%%% 首先我们显然可以将所有能够得到的字符串分成六类:\(\varnothing,\text{*},s, ...

  8. HDU 5322 Hope

    HDU 5322 Hope 考虑 $ dp[n] $ 表示 长度为 $ n $ 的所有排列的答案. 首先,对于一个排列来说,如果最大值在 $ i $ 位置,那么前 $ i - 1 $ 个数必然与 $ ...

  9. Peaks Gym 100365H

    Peaks ( Gym 100365H ) 这题nk做法还挺正常的..后面那个循环就很恶心了 考虑 dp[i][j] 表示长度为i的排列,恰好有k个峰的方案数量. 然后转移就是把 i 插入 i-1 的 ...

  10. 关于 KB/KiB、MB/MiB

    ermmm--怎么说呢,这个非常容易搞混,那就写篇 blog 澄清一下吧-- 首先贴上百度百科的官方定义 根据国际单位制标准,1KB = 1000B(字节, Byte). 根据按照 IEC 命名标准 ...