[Abp vNext 源碼分析] - 8. 審計日誌

来源:https://www.cnblogs.com/myzony/archive/2019/10/08/11634428.html
-Advertisement-
Play Games

一、簡要說明 ABP vNext 當中的審計模塊早在 "依賴註入與攔截器" 一文中有所提及,但沒有詳細的對其進行分析。 審計模塊是 ABP vNext 框架的一個基本組件,它能夠提供一些實用日誌記錄。不過這裡的日誌不是說系統日誌,而是說介面每次調用之後的執行情況(執行時間、傳入參數、異常信息、請求 ...


一、簡要說明

ABP vNext 當中的審計模塊早在 依賴註入與攔截器一文中有所提及,但沒有詳細的對其進行分析。

審計模塊是 ABP vNext 框架的一個基本組件,它能夠提供一些實用日誌記錄。不過這裡的日誌不是說系統日誌,而是說介面每次調用之後的執行情況(執行時間、傳入參數、異常信息、請求 IP)。

除了常規的日誌功能以外,關於 實體聚合 的審計欄位介面也是存放在審計模塊當中的。(創建人創建時間修改人修改時間刪除人刪除時間

二、源碼分析

2.1. 審計日誌攔截器

2.1.1 審計日誌攔截器的註冊

Volo.Abp.Auditing 的模塊定義十分簡單,主要是提供了 審計日誌攔截器 的註冊功能。下麵代碼即在組件註冊的時候,會調用 AuditingInterceptorRegistrar.RegisterIfNeeded 方法來判定是否為實現類型(ImplementationType) 註入審計日誌攔截器。

public class AbpAuditingModule : AbpModule
{
    public override void PreConfigureServices(ServiceConfigurationContext context)
    {
        context.Services.OnRegistred(AuditingInterceptorRegistrar.RegisterIfNeeded);
    }
}

跳轉到具體的實現,可以看到內部會結合三種類型進行判斷。分別是 AuditedAttributeIAuditingEnabledDisableAuditingAttribute

前兩個作用是,只要類型標註了 AuditedAttribute 特性,或者是實現了 IAuditingEnable 介面,都會為該類型註入審計日誌攔截器。

DisableAuditingAttribute 類型則相反,只要類型上標註了該特性,就不會啟用審計日誌攔截器。某些介面需要 提升性能 的話,可以嘗試使用該特性禁用掉審計日誌功能。

public static class AuditingInterceptorRegistrar
{
    public static void RegisterIfNeeded(IOnServiceRegistredContext context)
    {
        // 滿足條件時,將會為該類型註入審計日誌攔截器。
        if (ShouldIntercept(context.ImplementationType))
        {
            context.Interceptors.TryAdd<AuditingInterceptor>();
        }
    }

    private static bool ShouldIntercept(Type type)
    {
        // 首先判斷類型上面是否使用了輔助類型。
        if (ShouldAuditTypeByDefault(type))
        {
            return true;
        }

        // 如果任意方法上面標註了 AuditedAttribute 特性,則仍然為該類型註入攔截器。
        if (type.GetMethods().Any(m => m.IsDefined(typeof(AuditedAttribute), true)))
        {
            return true;
        }

        return false;
    }

    //TODO: Move to a better place
    public static bool ShouldAuditTypeByDefault(Type type)
    {
        // 下麵就是根據三種輔助類型進行判斷,是否為當前 type 註入審計日誌攔截器。
        if (type.IsDefined(typeof(AuditedAttribute), true))
        {
            return true;
        }

        if (type.IsDefined(typeof(DisableAuditingAttribute), true))
        {
            return false;
        }

        if (typeof(IAuditingEnabled).IsAssignableFrom(type))
        {
            return true;
        }

        return false;
    }
}

2.1.2 審計日誌攔截器的實現

審計日誌攔截器的內部實現,主要使用了三個類型進行協同工作。它們分別是負責管理審計日誌信息的 IAuditingManager,負責創建審計日誌信息的 IAuditingHelper,還有統計介面執行時常的 Stopwatch

整個審計日誌攔截器的大體流程如下:

  1. 首先是判定 MVC 審計日誌過濾器是否進行處理。
  2. 再次根據特性,和類型進行二次驗證是否應該創建審計日誌信息。
  3. 根據調用信息,創建 AuditLogInfoAuditLogActionInfo 審計日誌信息。
  4. 調用 StopWatch 的計時方法,如果出現了異常則將異常信息添加到剛纔構建的 AuditLogInfo 對象中。
  5. 無論是否出現異常,都會進入 finally 語句塊,這個時候會調用 StopWatch 實例的停止方法,並統計完成執行時間。
public override async Task InterceptAsync(IAbpMethodInvocation invocation)
{
    if (!ShouldIntercept(invocation, out var auditLog, out var auditLogAction))
    {
        await invocation.ProceedAsync();
        return;
    }

    // 開始進行計時操作。
    var stopwatch = Stopwatch.StartNew();

    try
    {
        await invocation.ProceedAsync();
    }
    catch (Exception ex)
    {
        // 如果出現了異常,一樣的將異常信息添加到審計日誌結果中。
        auditLog.Exceptions.Add(ex);
        throw;
    }
    finally
    {
        // 統計完成,並將信息加入到審計日誌結果中。
        stopwatch.Stop();
        auditLogAction.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds);
        auditLog.Actions.Add(auditLogAction);
    }
}

可以看到,只有當 ShouldIntercept() 方法返回 true 的時候,下麵的統計等操作才會被執行。

protected virtual bool ShouldIntercept(
    IAbpMethodInvocation invocation, 
    out AuditLogInfo auditLog, 
    out AuditLogActionInfo auditLogAction)
{
    auditLog = null;
    auditLogAction = null;

    if (AbpCrossCuttingConcerns.IsApplied(invocation.TargetObject, AbpCrossCuttingConcerns.Auditing))
    {
        return false;
    }

    // 如果沒有獲取到 Scop,則返回 false。
    var auditLogScope = _auditingManager.Current;
    if (auditLogScope == null)
    {
        return false;
    }

    // 進行二次判斷是否需要存儲審計日誌。
    if (!_auditingHelper.ShouldSaveAudit(invocation.Method))
    {
        return false;
    }

    // 構建審計日誌信息。
    auditLog = auditLogScope.Log;
    auditLogAction = _auditingHelper.CreateAuditLogAction(
        auditLog,
        invocation.TargetObject.GetType(),
        invocation.Method, 
        invocation.Arguments
    );

    return true;
}

2.2 審計日誌的持久化

大體流程和我們上面說的一樣,不過好像缺少了重要的一步,那就是 持久化操作。你可以在 Volo.Abp.Auditing 模塊發現有 IAuditingStore 介面的定義,但是它的 SaveAsync() 方法卻沒有在攔截器內部被調用。同樣在 MVC 的審計日誌過濾器實現,你也會發現沒有調用持久化方法。

那麼我們的審計日誌是在什麼時候被持久化的呢?找到 SaveAsync() 被調用的地方,發現 ABP vNext 實現了一個審計日誌的 ASP.NET Core 中間件。

在這個中間件內部的實現比較簡單,首先通過一個判定方法,決定是否為本次請求執行 IAuditingManager.BeginScope() 方法。如果判定通過,則執行,否則不僅行任何操作。

public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
    if (!ShouldWriteAuditLog(context))
    {
        await next(context);
        return;
    }

    using (var scope = _auditingManager.BeginScope())
    {
        try
        {
            await next(context);
        }
        finally
        {
            await scope.SaveAsync();
        }
    }
}

可以看到,在這裡 ABP vNext 使用 IAuditingManager 構建,調用其 BeginScope() 構建了一個 IAuditLogSaveHandle 對象,並使用其提供的 SaveAsync() 方法進行持久化操作。

2.2.1 嵌套的持久化操作

在構造出來的 IAuditLogSaveHandle 對象裡面,還是使用的 IAuditingManager 的預設實現 AuditingManager 所提供的 SaveAsync() 方法進行持久化

閱讀源碼之後,發現了下麵兩個問題:

  1. IAuditingManager 沒有將持久化方法公開 出來,而是作為一個 protected 級別的方法。
  2. 為什麼還要藉助 IAuditLogSaveHandle 間接地調用 管理器的持久化方法。

這就要從中間件的代碼說起了,可以看到它是構造出了一個可以被釋放的 IAuditLogSaveHandle 對象。ABP vNext 這樣做的目的,就是可以嵌套多個 Scope,即 只在某個範圍內 才將審計日誌記錄下來。這種特性類似於 工作單元 的用法,其底層實現是 之前文章 講過的 IAmbientScopeProvider 對象。

例如在某個應用服務內部,我可以這樣寫代碼:

using (var scope = _auditingManager.BeginScope())
{
    await myAuditedObject1.DoItAsync(new InputObject { Value1 = "我是內部嵌套測試方法1。", Value2 = 5000 });
    using (var scope2 = _auditingManager.BeginScope())
    {
        await myAuditedObject1.DoItAsync(new InputObject {Value1 = "我是內部嵌套測試方法2。", Value2 = 10000});
        await scope2.SaveAsync();
    }
    await scope.SaveAsync();
}

想一下之前的代碼,在攔截器內部,我們是通過 IAuditingManager.Current 拿到當前可用的 IAuditLogScope ,而這個 Scope 就是在調用 IAuditingManager.BeginScope() 之後生成的

2.2.3 最終的持久化代碼

通過上述的流程,我們得知最後的審計日誌信息會通過 IAuditingStore 進行持久化。ABP vNext 為我們提供了一個預設的 SimpleLogAuditingStore 實現,其內部就是調用 ILogger 將信息輸出。如果需要將審計日誌持久化到資料庫,你可以實現 IAUditingStore 介面,覆蓋原有實現 ,或者使用 ABP vNext 提供的 Volo.Abp.AuditLogging 模塊。

2.3 審計日誌的序列化

審計日誌的序列化處理是在 IAuditingHelper 的預設實現內部被使用,可以看到構建審計日誌的方法內部,通過自定義的序列化器來將 Action 的參數進行序列化處理,方便存儲。

public virtual AuditLogActionInfo CreateAuditLogAction(
    AuditLogInfo auditLog,
    Type type, 
    MethodInfo method, 
    IDictionary<string, object> arguments)
{
    var actionInfo = new AuditLogActionInfo
    {
        ServiceName = type != null
            ? type.FullName
            : "",
        MethodName = method.Name,
        // 序列化參數信息。
        Parameters = SerializeConvertArguments(arguments),
        ExecutionTime = Clock.Now
    };

    //TODO Execute contributors

    return actionInfo;
}

protected virtual string SerializeConvertArguments(IDictionary<string, object> arguments)
{
    try
    {
        if (arguments.IsNullOrEmpty())
        {
            return "{}";
        }

        var dictionary = new Dictionary<string, object>();

        foreach (var argument in arguments)
        {
            // 忽略的代碼,主要作用是構建參數字典。
        }

        // 調用序列化器,序列化 Action 的調用參數。
        return AuditSerializer.Serialize(dictionary);
    }
    catch (Exception ex)
    {
        Logger.LogException(ex, LogLevel.Warning);
        return "{}";
    }
}

下麵就是具體序列化器的代碼:

public class JsonNetAuditSerializer : IAuditSerializer, ITransientDependency
{
    protected AbpAuditingOptions Options;

    public JsonNetAuditSerializer(IOptions<AbpAuditingOptions> options)
    {
        Options = options.Value;
    }

    public string Serialize(object obj)
    {
        // 使用 JSON.NET 進行序列化操作。
        return JsonConvert.SerializeObject(obj, GetSharedJsonSerializerSettings());
    }

    // ... 省略的代碼。
}

2.4 審計日誌的配置參數

針對審計日誌相關的配置參數的定義,都存放在 AbpAuditingOptions 當中。下麵我會針對各個參數的用途,對其進行詳細的說明。

public class AbpAuditingOptions
{
    //TODO: Consider to add an option to disable auditing for application service methods?

    // 該參數目前版本暫未使用,為保留參數。
    public bool HideErrors { get; set; }

    // 是否啟用審計日誌功能,預設值為 true。
    public bool IsEnabled { get; set; }

    // 審計日誌的應用程式名稱,預設值為 null,主要在構建 AuditingInfo 被使用。
    public string ApplicationName { get; set; }

    // 是否為匿名請求記錄審計日誌預設值 true。
    public bool IsEnabledForAnonymousUsers { get; set; }

    // 審計日誌功能的協作者集合,預設添加了 AspNetCoreAuditLogContributor 實現。
    public List<AuditLogContributor> Contributors { get; }

    // 預設的忽略類型,主要在序列化時使用。
    public List<Type> IgnoredTypes { get; }

    // 實體類型選擇器。
    public IEntityHistorySelectorList EntityHistorySelectors { get; }

    //TODO: Move this to asp.net core layer or convert it to a more dynamic strategy?
    // 是否為 Get 請求記錄審計日誌,預設值 false。
    public bool IsEnabledForGetRequests { get; set; }

    public AbpAuditingOptions()
    {
        IsEnabled = true;
        IsEnabledForAnonymousUsers = true;
        HideErrors = true;

        Contributors = new List<AuditLogContributor>();

        IgnoredTypes = new List<Type>
        {
            typeof(Stream),
            typeof(Expression)
        };

        EntityHistorySelectors = new EntityHistorySelectorList();
    }
}

2.4 實體相關的審計信息

在文章開始就談到,除了對 HTTP 請求有審計日誌記錄以外,ABP vNext 還提供了實體審計信息的記錄功能。所謂的實體的審計信息,指的就是實體繼承了 ABP vNext 提供的介面之後,ABP vNext 會自動維護實現的介面欄位,不需要開發人員自己再進行處理。

這些介面包括創建實體操作的相關信息 IHasCreationTimeIMayHaveCreatorICreationAuditedObject 以及刪除實體時,需要記錄的相關信息介面 IHasDeletionTimeIDeletionAuditedObject 等。除了審計日誌模塊定義的類型以外,在 Volo.Abp.Ddd.Domain 模塊的 Auditing 裡面也有很多審計實體的預設實現。

我在這裡就不再一一列舉,下麵僅快速講解一下 ABP vNext 是如何通過這些介面,實現對審計欄位的自動維護的。

在審計日誌模塊的內部,我們看到一個介面名字叫做 IAuditPropertySetter,它提供了三個方法,分別是:

public interface IAuditPropertySetter
{
    void SetCreationProperties(object targetObject);

    void SetModificationProperties(object targetObject);

    void SetDeletionProperties(object targetObject);
}

所以,這幾個方法就是用於設置創建信息、修改信息、刪除信息的。現在跳轉到預設實現 AuditPropertySetter,隨便找一個 SetCreationTime() 方法。該方法內部首先是判斷傳入的 object 是否實現了 IHasCreationTime 介面,如果實現了對其進行強制類型轉換,然後賦值即可。

private void SetCreationTime(object targetObject)
{
    if (!(targetObject is IHasCreationTime objectWithCreationTime))
    {
        return;
    }

    if (objectWithCreationTime.CreationTime == default)
    {
        objectWithCreationTime.CreationTime = Clock.Now;
    }
}

其他幾個 Set 方法大同小異,那我們看一下有哪些地方使用到了上述三個方法。

可以看到使用者就包含有 EF Core 模塊和 MongoDB 模塊,這裡我以 EF Core 模塊為例,猜測應該是傳入了實體對象過來。

果不其然...查看這個方法的調用鏈,發現是 DbContext 每次進行 SaveChanges/SaveChangesAsync 的時候,就會對實體進行審計欄位自動賦值操作。

三、總結

審計日誌是 ABP vNext 為我們提供的一個可選組件,當開啟審計日誌功能後,我們可以根據審計日誌信息快速定位問題。但審計日誌的開啟,也會較大的影響性能,因為每次請求都會創建審計日誌信息,之後再進行持久化。因此在使用審計日誌功能時,可以結合 DisableAuditingAttribute 特性和 IAuditingManager.BeginScope(),按需開啟審計日誌功能。

四、點擊我跳轉到文章目錄


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在學習Python的是後遇到點小問題,記錄下來以後忘了再來看看。 一. python2 和python3在繼承父類的時候是不同的。super() 是一個特殊的函數,幫助Python將父類和子類關聯起來。在Python3中,直接使用如下代碼: Python3 在Python2中有兩種方法: 第一種 第 ...
  • Python 入門 之 反射 通過字元串操作對象的屬性和方法 對象的角度使用反射 類的角度使用反射 當前模塊使用反射 其他模塊使用反射 反射的應用場景 ...
  • ``` include int main(void) { int arr[10]={5,4,7,9,2,3,1,6,10,8}; //定義一個位排序的數組 int i; //定義迴圈次數 int n = 0; //定義排序次數 int length=10; //定義數組長度 while(n arr[ ...
  • 一、random模塊 random模塊是用來生成隨機數的。 練習:生成隨機驗證碼 import random def v_code(): code = '' for i in range(5): num=random.randint(0,9) alf=chr(random.randint(65,90 ...
  • 介面表示一種能力,實現了一個介面,即擁有一種能力。 BeanDefinition與Bean的關係, 就好比類與對象的關係. 類在spring的數據結構就是BeanDefinition.根據BeanDefinition得到的對象就是我們需要的Bean. 我認為理解Bean與BeanDefinition ...
  • @EnableAutoConfiguration註解是Spring Boot中配置自動裝載的總開關。本文將從@EnableAutoConfiguration入手,嘗試分析Spring Boot對@AutoConfigurationImportSelector註解的處理過程。 ...
  • 在GitHub上有個項目,本來是作為自己研究學習.net core的Demo,沒想到很多同學在看,還給了很多星,所以覺得應該升成3.0,整理一下,寫成博分享給學習.net core的同學們。 項目名稱:Asp.NetCoreExperiment 項目地址:https://github.c... ...
  • 本文轉自:https://www.cnblogs.com/shangwater/p/5449470.html 錯誤:程式集綁定日誌記錄被關閉。 解決方法: 將IIS中的應用程式服務池中"啟動32位應用程式"設置為true。 錯誤描述: 部署到IIS中的應用程式出現如下錯誤(Win7 x64位 .ne ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...