前言:
近日在專案協同開發過程中出現了問題,資料出現了異常;其他人員懷疑專案資料丟失程式存在問題。於是通過排查程式提供的審計日誌最終還原了當時操作及原因。
可見審計日誌在排查、定位問題是相當有用的,那麼在.Net Core 如何來實現審計日誌呢?
接下來一步步來實現效果
一、審計日誌定義及作用
審計日誌:
維基百科: “審計跟蹤(也叫審計日誌)是與安全相關的按照時間順序的記錄,記錄集或者記錄源,它們提供了活動序列的文件證據,這些活動序列可以在任何時間影響一個特定的操作,步驟或其他”
作用:
1、快速定位問題耗時及效能情況
2、記錄呼叫時環境資訊:如瀏覽器、引數等
二、.Net Core 中實現審計日誌
那麼怎麼實現審計日誌呢?其實核心思想很簡單。包含以下步驟:
- 獲取呼叫介面方法時相關資訊
- 記錄當前介面耗時情況
- 儲存審計日誌資訊到資料庫中
那麼如何獲取呼叫介面時相關資訊呢?.Net Core中可以使用:過濾器、攔截器 實現。
本次示例中將採用過濾器實現審計日誌實現功能;主要流程如下
-
定義審計日誌資訊:
public class AuditInfo { /// <summary> /// 呼叫引數 /// </summary> public string Parameters { get; set; } /// <summary> /// 瀏覽器資訊 /// </summary> public string BrowserInfo { get; set; } /// <summary> /// 客戶端資訊 /// </summary> public string ClientName { get; set; } /// <summary> /// 客戶端IP地址 /// </summary> public string ClientIpAddress { get; set; } /// <summary> /// 執行耗時 /// </summary> public int ExecutionDuration { get; set; } /// <summary> /// 執行時間 /// </summary> public DateTime ExecutionTime { get; set; } /// <summary> /// 返回內容 /// </summary> public string ReturnValue { get; set; } /// <summary> /// 異常物件 /// </summary> public Exception Exception { get; set; } /// <summary> /// 方法名 /// </summary> public string MethodName { get; set; } /// <summary> /// 服務名 /// </summary> public string ServiceName { get; set; } /// <summary> /// 呼叫者資訊 /// </summary> public string UserInfo { get; set; } /// <summary> /// 自定義資料 /// </summary> public string CustomData { get; set; } }
- 實現審計日誌過濾器
using AuditLogDemo.Models; using AuditLogDemo.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Threading.Tasks; namespace AuditLogDemo.Fliters { public class AuditLogActionFilter : IAsyncActionFilter { /// <summary> /// 審計日誌服務物件 /// </summary> private readonly IAuditLogService _auditLogService; /// <summary> /// 登入使用者 /// </summary> private readonly ISession _Session; /// <summary> /// 日誌記錄 /// </summary> private readonly ILogger<AuditLogActionFilter> _logger; public AuditLogActionFilter( IAuditLogService auditLogService, ISession Session, ILogger<AuditLogActionFilter> logger ) { _Session = Session; _logger = logger; _auditLogService = auditLogService; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { // 判斷是否寫日誌 if (!ShouldSaveAudit(context)) { await next(); return; } //介面Type var type = (context.ActionDescriptor as ControllerActionDescriptor).ControllerTypeInfo.AsType(); //方法資訊 var method = (context.ActionDescriptor as ControllerActionDescriptor).MethodInfo; //方法引數 var arguments = context.ActionArguments; //開始計時 var stopwatch = Stopwatch.StartNew(); var auditInfo = new AuditInfo { UserInfo = _Session?.Id, ServiceName = type != null ? type.FullName.TruncateWithPostfix(EntityDefault.FieldsLength250) : "", MethodName = method.Name.TruncateWithPostfix(EntityDefault.FieldsLength250), ////請求引數轉Json Parameters = JsonConvert.SerializeObject(arguments), ExecutionTime = DateTime.Now, BrowserInfo = context.HttpContext.Request.Headers["User-Agent"].ToString().TruncateWithPostfix(EntityDefault.FieldsLength250), ClientIpAddress = context.HttpContext.Connection.RemoteIpAddress.ToString().TruncateWithPostfix(EntityDefault.FieldsLength50), //ClientName = _clientInfoProvider.ComputerName.TruncateWithPostfix(EntityDefault.FieldsLength100), Id = Guid.NewGuid().ToString() }; ActionExecutedContext result = null; try { result = await next(); if (result.Exception != null && !result.ExceptionHandled) { auditInfo.Exception = result.Exception; } } catch (Exception ex) { auditInfo.Exception = ex; throw; } finally { stopwatch.Stop(); auditInfo.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds); if (result != null) { switch (result.Result) { case ObjectResult objectResult: auditInfo.ReturnValue = JsonConvert.SerializeObject(objectResult.Value); break; case JsonResult jsonResult: auditInfo.ReturnValue = JsonConvert.SerializeObject(jsonResult.Value); break; case ContentResult contentResult: auditInfo.ReturnValue = contentResult.Content; break; } } Console.WriteLine(auditInfo.ToString()); //儲存審計日誌 await _auditLogService.SaveAsync(auditInfo); } } /// <summary> /// 是否需要記錄審計 /// </summary> /// <param name="context"></param> /// <returns></returns> private bool ShouldSaveAudit(ActionExecutingContext context) { if (!(context.ActionDescriptor is ControllerActionDescriptor)) return false; var methodInfo = (context.ActionDescriptor as ControllerActionDescriptor).MethodInfo; if (methodInfo == null) { return false; } if (!methodInfo.IsPublic) { return false; } if (methodInfo.GetCustomAttribute<AuditedAttribute>() != null) { return true; } if (methodInfo.GetCustomAttribute<DisableAuditingAttribute>() != null) { return false; } var classType = methodInfo.DeclaringType; if (classType != null) { if (classType.GetTypeInfo().GetCustomAttribute<AuditedAttribute>() != null) { return true; } if (classType.GetTypeInfo().GetCustomAttribute<AuditedAttribute>() != null) { return false; } } return false; } } }
該內容為實現審計日誌功能主要邏輯,通過過濾器獲取當前執行控制器、方法判斷是否需要記錄審計日誌;其他請求引數、客戶端ip等相關基本資訊組成審計日誌物件,並記錄呼叫時間。
- 註冊過濾器
public void ConfigureServices(IServiceCollection services) { services.AddControllers(options => { options.Filters.Add(typeof(AuditLogActionFilter)); }); //審計日誌儲存 services.AddDbContext<AuditLogDBContent>(options => { string conn = Configuration.GetConnectionString("LogDB"); options.UseSqlite(conn); }); }
到此審計日誌主要邏輯已經實現完成。是不是很簡單
三、總結
回過頭來看,在.net core 中需要統一監控或過濾時,可以採用過濾器(Filter)或攔截器來實現相關效果
.Net Core中 Filter 常件的有:Authorization Filter(認證過濾器),Resource Filter(資源過濾器),Exception Filter(異常過濾器),Action Filter(方法過濾器),Result Filter(結果過濾器)