1. 程式人生 > 其它 >.NET Core工程應用系列(1) 定製化Audit.NET實現自定義AuditTarget

.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這個框架還是非常強大的,這篇文章只探討了其中非常小的一個特性點,當然基於本文的思路,我們還可以實現更復雜的審計日誌資料物件過濾邏輯,可擴充套件性還是很好的。