ASP.NET?Core實現(xiàn)動態(tài)審計日志功能
前言
最近一直在寫 Go 和 Python ,好久沒寫 C# ,重新回來寫 C# 代碼時竟有一種親切感~
說回正題。
在當(dāng)今這個數(shù)字化迅速發(fā)展的時代,每一個操作都可能對業(yè)務(wù)產(chǎn)生深遠(yuǎn)的影響,無論是對數(shù)據(jù)的簡單查詢,還是對系統(tǒng)配置的修改。在這樣的背景下,審計日志不僅僅是一種遵循最佳實踐的手段,更是確保數(shù)據(jù)安全、提高系統(tǒng)透明度、促進(jìn)責(zé)任歸屬明晰的關(guān)鍵工具。通過詳細(xì)記錄誰在何時對系統(tǒng)進(jìn)行了何種操作,審計日志幫助組織追蹤用戶活動,分析系統(tǒng)問題,甚至在發(fā)生安全事件時,提供必要的線索進(jìn)行調(diào)查。
實現(xiàn)審計日志的方法多樣,但如何在不干擾主業(yè)務(wù)邏輯的同時,高效地集成這一功能,是開發(fā)者們面臨的一大挑戰(zhàn)。本文著重探討如何借鑒面向切面編程(Aspect-Oriented Programming, AOP)的設(shè)計思想,在ASP.NET Core應(yīng)用中以最小化代碼侵入性實現(xiàn)動態(tài)審計日志功能。AOP允許我們通過預(yù)定義的模式,如日志記錄、性能統(tǒng)計和安全控制,以聲明的方式增強代碼功能,而無需修改實際的業(yè)務(wù)邏輯代碼。
本文將指導(dǎo)讀者從概念的理解到具體的實施,再到最終的數(shù)據(jù)持久化處理,特別是如何利用MongoDB這一強大的NoSQL數(shù)據(jù)庫來持久化審計日志數(shù)據(jù)。無論你是剛剛接觸ASP.NET Core的新手,還是尋求為現(xiàn)有項目增加審計功能的資深開發(fā)者,本文都將提供從理論到實踐的全面指導(dǎo)。通過本文,你將學(xué)習(xí)到如何設(shè)計和實現(xiàn)一個靈活、可擴展的審計日志系統(tǒng),同時保持對主業(yè)務(wù)邏輯的最小化干擾。
讓我們開始這一旅程,一步步探索如何在ASP.NET Core應(yīng)用中集成高效、靈活的審計日志機制,利用AOP設(shè)計思想實現(xiàn)高度解耦和動態(tài)增強的系統(tǒng)功能。
審計日志基礎(chǔ)
定義和用途
審計日志有助于追蹤用戶的操作行為、數(shù)據(jù)變更記錄以及系統(tǒng)的安全性分析等。
常用的審計日志有這些類型。
- 操作審計:記錄用戶對系統(tǒng)的所有操作,例如登錄、登出、數(shù)據(jù)增刪改查等。
- 數(shù)據(jù)審計:記錄數(shù)據(jù)的變更詳情,如記錄數(shù)據(jù)修改前后的值。
- 安全審計:記錄安全相關(guān)事件,如失敗的登錄嘗試、權(quán)限變更等。
- 性能審計:記錄關(guān)鍵操作的性能數(shù)據(jù),幫助分析系統(tǒng)瓶頸。
本文的代碼以實現(xiàn)操作審計為例。
模型定義&關(guān)鍵信息
審計日志是系統(tǒng)安全和管理的關(guān)鍵部分,它幫助我們理解系統(tǒng)內(nèi)發(fā)生了什么、何時發(fā)生、由誰觸發(fā)。為了實現(xiàn)這一目標(biāo),審計日志記錄需要包含幾個關(guān)鍵的組成部分。
- EventId 是每條審計記錄的唯一標(biāo)識符。就像每個人都有一個獨一無二的身份證號一樣,每條審計日志也有一個獨特的EventId。這使我們能夠輕松地找到和引用特定的審計事件。
- EventType 描述了發(fā)生的事件類型。這告訴我們這條記錄是關(guān)于什么的——是用戶登錄、數(shù)據(jù)修改,還是權(quán)限更改等。通過查看EventType,我們可以快速了解記錄的核心信息,而無需深入研究細(xì)節(jié)。
- UserId 是觸發(fā)事件的用戶的標(biāo)識。在審計日志中記錄UserId非常重要,因為它幫助我們追蹤誰負(fù)責(zé)了什么操作。如果發(fā)現(xiàn)了問題或者不當(dāng)行為,我們可以通過UserId來確定責(zé)任人。
設(shè)計審計日志模型
AuditLog 類
新建 AuditLog.cs
類,每個字段都有注釋,我就不再贅述了。
public class AuditLog { /// <summary> /// 事件唯一標(biāo)識 /// </summary> public string EventId { get; set; } /// <summary> /// 事件類型(例如:登錄、登出、數(shù)據(jù)修改等) /// </summary> public string EventType { get; set; } /// <summary> /// 執(zhí)行操作的用戶標(biāo)識 /// </summary> public string UserId { get; set; } /// <summary> /// 執(zhí)行操作的用戶名 /// </summary> public string Username { get; set; } /// <summary> /// 事件發(fā)生的時間戳 /// </summary> public DateTime Timestamp { get; set; } /// <summary> /// 用戶的IP地址 /// </summary> public string? IPAddress { get; set; } /// <summary> /// 被操作的實體名稱 /// </summary> public string EntityName { get; set; } /// <summary> /// 被操作的實體標(biāo)識 /// </summary> public string EntityId { get; set; } /// <summary> /// 修改前的數(shù)據(jù),可根據(jù)實際情況以JSON格式存儲 /// </summary> public string? OriginalValues { get; set; } /// <summary> /// 修改后的數(shù)據(jù),可根據(jù)實際情況以JSON格式存儲 /// </summary> public string? CurrentValues { get; set; } /// <summary> /// 具體的更改內(nèi)容,可根據(jù)實際情況以JSON格式存儲 /// </summary> public string? Changes { get; set; } /// <summary> /// 事件描述 /// </summary> public string? Description { get; set; } }
捕獲審計日志
IAuditLogService 接口
先寫一個接口,用來操作審計日志。使用接口可以保持代碼的整潔和重用,同時也便于將來對審計日志記錄邏輯進(jìn)行擴展或修改。
為了簡單起見,目前這里我們只寫了一個記錄的方法。
public interface IAuditLogService { Task LogAsync(AuditLog auditLog); }
之后在依賴注入容器里注冊(假設(shè)實現(xiàn)類的名稱為 AuditLogService
)
builder.Services.AddScope<IAuditLogService, AuditLogService>();
這個設(shè)計既保持了代碼的清晰與簡潔,也為將來可能的需求變更(如改變審計日志的存儲方式、增加審計字段等)提供了足夠的靈活性。
具體實現(xiàn)會在后續(xù)的數(shù)據(jù)持久化部分介紹。
ActionFilter 方式
在ASP.NET Core中,Action過濾器提供了一種強大的機制,允許我們在控制器的動作執(zhí)行前后插入自定義邏輯。
我們可以在不修改現(xiàn)有業(yè)務(wù)邏輯代碼的情況下,自動地捕獲用戶的操作以及數(shù)據(jù)的更改。這種方式充分利用了AOP的思想,實現(xiàn)了代碼的最小化侵入。
創(chuàng)建 AuditLogAttribute 類
直接上代碼了,繼承自 ActionFilterAttribute
類,可以實現(xiàn)一個 Action 過濾器的特性,其中 EventType
和 EntityName
我設(shè)計成需要手動指定,其他的屬性可以通過各種方法來獲取。
public class AuditLogAttribute : ActionFilterAttribute { public string EventType { get; set; } public string EntityName { get; set; } public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var sp = context.HttpContext.RequestServices; var ctxItems = context.HttpContext.Items; try { var authService = sp.GetRequiredService<AuthService>(); // 在操作執(zhí)行前 var executedContext = await next(); // 在操作執(zhí)行后 // 獲取當(dāng)前用戶的身份信息 var user = await authService.GetUserFromJwt(executedContext.HttpContext.User); // 構(gòu)造AuditLog對象 var auditLog = new AuditLog { EventId = Guid.NewGuid().ToString(), EventType = this.EventType, UserId = user.UserId, Username = user.Username, Timestamp = DateTime.UtcNow, IPAddress = GetIpAddress(executedContext.HttpContext), EntityName = this.EntityName, EntityId = ctxItems["AuditLog_EntityId"]?.ToString() ?? "", OriginalValues = ctxItems["AuditLog_OriginalValues"]?.ToString(), CurrentValues = ctxItems["AuditLog_CurrentValues"]?.ToString(), Changes = ctxItems["AuditLog_Changes"]?.ToString(), Description = $"操作類型:{this.EventType},實體名稱:{this.EntityName}", }; var auditService = sp.GetRequiredService<IAuditLogService>(); await auditService.LogAsync(auditLog); } catch (Exception ex) { var logger = sp.GetRequiredService<ILogger<AuditLogAttribute>>(); logger.LogError(ex, "An error occurred while logging audit information."); } } }
注意事項
- 異常處理:考慮到日志記錄不應(yīng)影響主要業(yè)務(wù)流程的執(zhí)行,需要添加異常處理邏輯,確保即使日志記錄過程中發(fā)生異常,也不會干擾到正常的業(yè)務(wù)邏輯。
- 性能問題:雖然已經(jīng)在異步方法中記錄審計日志,但如果審計日志的記錄過程很慢,可能會略微延遲響應(yīng)時間。可以使用批處理、緩存來異步寫入數(shù)據(jù)庫,或者將記錄邏輯放到后臺任務(wù)、消息隊列中。
獲取IP地址
通過HttpContext.Connection.RemoteIpAddress
屬性可以獲取 IP 地址,但如果應(yīng)用部署在了代理服務(wù)器后面(例如使用了負(fù)載均衡器),直接獲取的IP地址可能是代理服務(wù)器的地址,而不是客戶端的真實IP地址。
所以這里我封裝了 GetIpAddress
方法
private string? GetIpAddress(HttpContext httpContext) { // 首先檢查X-Forwarded-For頭(當(dāng)應(yīng)用部署在代理后面時) var forwardedFor = httpContext.Request.Headers["X-Forwarded-For"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(forwardedFor)) { return forwardedFor.Split(',').FirstOrDefault(); // 可能包含多個IP地址 } // 如果沒有X-Forwarded-For頭,或者需要直接獲取連接的遠(yuǎn)程IP地址 return httpContext.Connection.RemoteIpAddress?.ToString(); }
首先嘗試從X-Forwarded-For
請求頭中獲取IP地址,這是一個標(biāo)準(zhǔn)的HTTP頭,用于識別通過HTTP代理或負(fù)載均衡器發(fā)送請求的客戶端的原始IP地址。如果請求沒有經(jīng)過代理,或者想要獲取代理服務(wù)器的地址,那么它會回退到使用HttpContext.Connection.RemoteIpAddress
。
X-Forwarded-For
可能包含多個IP地址(如果請求通過多個代理傳遞),因此代碼中使用了Split(',')
來處理這種情況,并且僅取第一個IP地址作為客戶端的真實IP地址。
使用方法
經(jīng)過封裝后可以很方便的使用這個審計功能了,只需要在接口上添加一行代碼就可以實現(xiàn)審計功能。
[AuditLog(EventType = nameof(SetSubTaskFeedback), EntityName = nameof(SubTask))] [HttpPost("sub-tasks/{subId}/set-feedback")] public async Task<ApiResponse> SetSubTaskFeedback(string subId, [FromBody] SubTaskFeedbackDto dto) {}
手動記錄方式
盡管使用Action過濾器是一種高效的自動化方式,但在某些情況下,需要更精細(xì)地控制審計日志的記錄。這時候只能修改接口代碼,在業(yè)務(wù)邏輯里加入審計日志記錄。
這種方式雖然需要直接修改業(yè)務(wù)代碼,但它提供了最大的靈活性和控制能力。
這個代碼就沒什么特別的了,直接在接口中調(diào)用 IAuditLogService
的 LogAsync
方法來記錄審計日志即可。
通過 HttpContext 共享數(shù)據(jù)
有些參數(shù)是很難在 ActionFilter 里自動獲取到的,這些往往跟業(yè)務(wù)邏輯是有關(guān)的,這時候 HttpContext 就成為了一個理想的橋梁。
我們可以將一些臨時數(shù)據(jù),比如操作前的數(shù)據(jù)快照,存儲在 HttpContext.Items
中,然后在過濾器中訪問這些數(shù)據(jù)來完成審計日志的記錄。這種方法不僅保持了代碼的解耦,還允許我們靈活地在應(yīng)用的不同部分共享數(shù)據(jù)。
HttpContext.Items
是一個鍵值對集合,可用于在一個請求的生命周期內(nèi)共享數(shù)據(jù)。
這樣在接口中的代碼就是
HttpContext.Items["AuditLog_OriginalValues"] = item.FeedbackId; HttpContext.Items["AuditLog_CurrentValues"] = dto.FeedbackId; HttpContext.Items["AuditLog_Changes"] = $"更新反饋結(jié)果 {item.FeedbackId} -> {dto.FeedbackId}";
注意事項
- 確保業(yè)務(wù)邏輯和
AuditLogAttribute
中使用的鍵(如AuditLog_OriginalValues
)唯一且一致,以避免潛在的沖突。這里最好是自己封裝一個 class 來提供這些 const ; - 如果業(yè)務(wù)邏輯抽象到了 service 層,則需要注入
IHttpContextAccessor
才能訪問 HttpContext ,這個服務(wù)可以通過services.AddHttpContextAccessor()
來注冊;
日志持久化
審計日志的有效持久化是確保長期安全和合規(guī)性的關(guān)鍵。
選擇存儲方案
在選擇最合適的存儲方案時,需要考慮數(shù)據(jù)的重要性、查詢的頻率、成本以及維護(hù)的復(fù)雜性等多個因素。
關(guān)系型數(shù)據(jù)庫(RDS)
關(guān)系型數(shù)據(jù)庫,如MySQL、PostgreSQL等,以其穩(wěn)定性和成熟性受到廣泛認(rèn)可。它們提供了嚴(yán)格的數(shù)據(jù)完整性保障和復(fù)雜查詢的強大能力,適合需要執(zhí)行復(fù)雜分析和報告的審計日志。
- 優(yōu)點:數(shù)據(jù)結(jié)構(gòu)化、支持復(fù)雜查詢、成熟的管理工具。
- 缺點:相對較高的成本、可能需要復(fù)雜的架構(gòu)來支持大規(guī)模數(shù)據(jù)。
NoSQL數(shù)據(jù)庫
NoSQL數(shù)據(jù)庫,如MongoDB、Cassandra等,提供了靈活的數(shù)據(jù)模型和良好的橫向擴展能力,適合于結(jié)構(gòu)多變或數(shù)據(jù)量巨大的審計日志。
- 優(yōu)點:高可擴展性、靈活的數(shù)據(jù)模型、快速的寫入速度。
- 缺點:查詢功能相對有限、數(shù)據(jù)一致性模型較弱。
文件系統(tǒng)
直接將審計日志寫入文件系統(tǒng)是最直接的存儲方式,適用于日志量不是特別大或?qū)Σ樵冃枨蟛桓叩膱鼍啊?/p>
- 優(yōu)點:實現(xiàn)簡單、成本低廉、易于遷移;
- 缺點:查詢和分析不便、難以管理大量日志文件、擴展性有限。
每種存儲方案都有其適用場景,因此選擇哪一種方案應(yīng)根據(jù)具體需求和資源情況綜合考慮。對于需要快速寫入和高度可擴展的審計日志系統(tǒng),NoSQL數(shù)據(jù)庫是一個不錯的選擇。
因此本文選擇了 MongoDB 來記錄日志。
選擇MongoDB作為審計日志的存儲方案,不僅因為它的高性能和可擴展性,還因為它支持靈活的文檔數(shù)據(jù)模型,使得存儲非結(jié)構(gòu)化或半結(jié)構(gòu)化的審計數(shù)據(jù)變得簡單。
實現(xiàn) AuditLogMongoService
在 C# 中使用 MongoDB 非常簡單。
需要先添加 MongoDB.Driver 的 nuget 包
dotnet add MongoDB.Driver
直接上代碼吧,
public class AuditLogMongoService : IAuditLogService { private readonly IMongoCollection<AuditLog> _auditLogs; public AuditLogMongoService(string connectionString, string databaseName) { var client = new MongoClient(connectionString); var database = client.GetDatabase(databaseName); _auditLogs = database.GetCollection<AuditLog>("audit_logs"); } public async Task LogAsync(AuditLog auditLog) { await _auditLogs.InsertOneAsync(auditLog); } }
準(zhǔn)備連接字符串&注冊服務(wù)
為了避免硬編碼,將連接字符串放在配置文件(appsettings.json
)里
"ConnectionStrings": { "Redis": "redis:6379", "MongoDB": "mongodb://username:password@path-to-mongo:27017" }
注冊服務(wù)
builder.Services.AddSingleton<IAuditLogService>(sp => new AuditLogMongoService(builder.Configuration.GetConnectionString("MongoDB"), "db_name"));
搞定~
部署 MongoDB
附上 MongoDB 的部署方法吧,我這里使用 docker ,很方便
version: '3.1' services: mongo: image: mongo:4.4.6 restart: always volumes: - ./data:/data/db environment: MONGO_INITDB_ROOT_USERNAME: username MONGO_INITDB_ROOT_PASSWORD: password ports: - 27017:27017 mongo-express: image: mongo-express restart: always environment: ME_CONFIG_MONGODB_ADMINUSERNAME: username ME_CONFIG_MONGODB_ADMINPASSWORD: password ME_CONFIG_MONGODB_URL: mongodb://username:password@mongo:27017/ ports: - 8081:8081
使用 docker-compose 來編排,映射了 27017 和 8081 端口
可以使用 8081 端口訪問 mongo-express 網(wǎng)頁服務(wù)
如何查看日志
- 使用 MongoDB Compass 這個軟件來查看數(shù)據(jù)
- 使用 mongo-express 服務(wù)可以在網(wǎng)頁上查看數(shù)據(jù)
小結(jié)
雖然是比較簡單的功能,不過使用 AOP 來實現(xiàn)用起來感覺還是蠻爽的,不得不說 AspNetCore 的功能確實豐富~
以上就是ASP.NET Core實現(xiàn)動態(tài)審計日志功能的詳細(xì)內(nèi)容,更多關(guān)于ASP.NET Core審計日志的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
FileUpload使用Javascript檢查擴展名是否有效實現(xiàn)思路
在JavaScript獲取FileUpload控件的文件路徑,并取得路徑中的文件擴展名,再與陣列中的擴展名比較,如果存在,說明上傳的文件是有效的,反之無效,感興趣的朋友可以了解下,或許對你有所幫助2013-02-02asp.net使用LINQ to SQL連接數(shù)據(jù)庫及SQL操作語句用法分析
這篇文章主要介紹了asp.net使用LINQ to SQL連接數(shù)據(jù)庫及SQL操作語句用法,較為詳細(xì)的分析了LINQ操作sql語句的功能、使用方法與相關(guān)注意事項,需要的朋友可以參考下2016-05-05HttpRequest的QueryString屬性 的一點認(rèn)識
我們開發(fā)asp.net程序獲取QueryString時,經(jīng)常性的遇到一些url編碼問題2012-11-11在.Net?Framework應(yīng)用中請求HTTP2站點的問題解析
隨著各大瀏覽器支持和蘋果的帶頭效應(yīng),HTTP2的應(yīng)用會越來越廣泛,但是規(guī)模龐大的.NET?Framework應(yīng)用卻也不能為了連接HTTP2就升級到NET?Core平臺。通過本文提供的方案,可以最小成本的實現(xiàn).NET?Framework應(yīng)用成功訪問HTTP2站點,感興趣的朋友跟隨小編一起看看吧2022-07-07ASP.NET2.0 WebRource,開發(fā)微調(diào)按鈕控件
ASP.NET2.0 WebRource,開發(fā)微調(diào)按鈕控件...2006-09-09asp.net Application_AcquireRequestState事件,導(dǎo)致Ajax客戶端不能加載
項目中使用Application_AcquireRequestState事件,來做一些用戶信息的驗證工作.2010-03-03