需求背景
最近在項目上需要增加對用戶操作進行審計日志記錄的功能,調研了一圈,在.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這個框架還是非常強大的,這篇文章只探討了其中非常小的一個特性點,當然基於本文的思路,我們還可以實現更復雜的審計日志數據對象過濾邏輯,可擴展性還是很好的。