ABP開發框架前後端開發系列---(7)系統審計日誌和登入日誌的管理
我們瞭解ABP框架內部自動記錄審計日誌和登入日誌的,但是這些資訊只是在相關的內部接口裡面進行記錄,並沒有一個管理介面供我們瞭解,但是其系統資料庫記錄了這些資料資訊,我們可以為它們設計一個檢視和匯出這些審計日誌和登入日誌的管理介面。本篇隨筆繼續ABP框架的系列介紹,一步步深入瞭解ABP框架的應用開發,介紹審計日誌和登入日誌的管理。
1、審計日誌和登入日誌的基礎
審計日誌,設定我們在訪問或者呼叫某個應用服務層介面的時候,橫切面流下的一系列操作記錄,其中記錄我們訪問的服務介面,引數,客戶端IP地址,訪問時間,以及異常等資訊,這些操作都是在ABP系統自動記錄的,如果我們需要遮蔽某些服務類或者介面,則這些就不會記錄在裡面,否則預設是記錄的。
登入日誌,這個就是使用者嘗試登入的時候,留下的記錄資訊,其中包括使用者的登入使用者名稱,ID,IP地址、登入時間,以及登入是否成功的狀態等資訊。
我們檢視系統資料庫,可以看到對應這兩個部分的日誌表,如下所示。
在ABP框架內部基礎專案Abp裡面,我們可以看到對應的領域物件實體和Store管理類,不過並沒有在應用層的對應服務和相關的DTO,我們需要實現一個審計日誌和登陸日誌的管理功能介面,介面效果如下所示。
我們搜尋ABP專案,查詢到審計日誌的相關類(包含領域物件實體和Store管理類),如下介面截圖。
同樣對於系統登入日誌物件,我們查詢到對應的領域實體和對應的Manger業務邏輯類。
這些也就代表它們都有底層的實現,但是沒有服務層應用和DTO物件,因此我們需要擴充套件這些內容才能夠管理顯示這些記錄資訊。
前面介紹過,預設的一般應用服務層和介面,都是會進行審計記錄寫入的,如果我們需要遮蔽某些應用服務層或者介面,不進行審計資訊的記錄,那麼需要使用特性標記[DisableAuditing]來管理。
如我們針對審計日誌應用層介面的訪問,我們不想讓它多餘的記錄,那麼就設定這個標記即可。
或者遮蔽某些介面
另外,如果我們不想公佈某些特殊的介面訪問,那麼我們可以通過標記 [RemoteService(false)] 進行遮蔽,這樣在Web API層就不會公佈對應的介面了。
如對於審計日誌的記錄,增刪改我們都不允許客戶端進行操作,那麼我們把對應的應用服務層介面遮蔽即可。
2、系統審計日誌和登入日誌的完善
前面介紹了,審計日誌和登陸日誌的處理,Abp系統只是做了一部分底層的內容,我們如果進行這些資訊的管理,我們需要完善它,增加對應的DTO類和應用服務層介面和介面實現。
首先我們根據底層的領域實體物件的屬性,複製過來作為對應DTO物件的屬性,並增加對應的分頁條件DTO物件,由於我們不需要進行建立,因此不需要增加Create***Dto物件類。
如對於審計日誌的DTO物件,我們定義如下所示(主要複製領域物件的屬性)。
而分頁處理的DTO物件如下所示,我們主要增加一個使用者名稱和建立時間區間的條件。
對於登入日誌的DTO物件,我們依葫蘆畫瓢,也是如此操作即可。
登入日誌的分頁物件Dto如下所示、
完善了這些DTO物件,下一步我們需要建立對應的應用服務層類,這樣我們才能在客戶端通過Web API獲取對應的資料。
首先我們來定義審計日誌應用服務類,如下所示。
[DisableAuditing] //遮蔽這個AppService的審計功能 [AbpAuthorize] public class AuditLogAppService : AsyncCrudAppService<AuditLog, AuditLogDto, long, AuditLogPagedDto>, IAuditLogAppService<AuditLogDto, long, AuditLogPagedDto> { private readonly IRepository<AuditLog, long> _repository; private readonly IAuditingStore _stroe; private readonly IRepository<User, long> _userRepository; public AuditLogAppService(IRepository<AuditLog, long> repository, IAuditingStore stroe, IRepository<User, long> userRepository) : base(repository) { _repository = repository; _stroe = stroe; _userRepository = userRepository; } ......
其中我們需要IRepository<User, long>用來轉義使用者ID為對應的使用者名稱,這樣對於我們顯示有幫助。
預設來說,這個應用服務層已經具有常規的增刪改查、分頁等基礎介面了,但是我們不需要對外公佈增刪改介面,我們需要重寫實現把它遮蔽。
/// <summary> /// 遮蔽建立介面 /// </summary> [RemoteService(false)] public override Task<AuditLogDto> Create(AuditLogDto input) { return base.Create(input); } /// <summary> /// 遮蔽更新介面 /// </summary> [RemoteService(false)] public override Task<AuditLogDto> Update(AuditLogDto input) { return base.Update(input); } /// <summary> /// 遮蔽刪除介面 /// </summary> [RemoteService(false)] public override Task Delete(EntityDto<long> input) { return base.Delete(input); }
那麼我們就剩下GetAll和Get兩個方法了,我們如果不需要轉義特殊內容,我們就可以不重寫它,但是我們這裡需要對使用者ID轉義為使用者名稱稱,那麼需要進行一個處理,如下所示。
[DisableAuditing] public override Task<PagedResultDto<AuditLogDto>> GetAll(AuditLogPagedDto input) { var result = base.GetAll(input); foreach (var item in result.Result.Items) { ConvertDto(item);//對使用者名稱稱進行解析 } return result; } [DisableAuditing] public override Task<AuditLogDto> Get(EntityDto<long> input) { var result = base.Get(input); ConvertDto(result.Result); return result; } /// <summary> /// 對記錄進行轉義 /// </summary> /// <param name="item">dto資料物件</param> /// <returns></returns> protected virtual void ConvertDto(AuditLogDto item) { //使用者名稱稱轉義 if (item.UserId.HasValue) { item.UserName = _userRepository.Get(item.UserId.Value).UserName; } //IP地址轉義 if (!string.IsNullOrEmpty(item.ClientIpAddress)) { item.ClientIpAddress = item.ClientIpAddress.Replace("::1", "127.0.0.1"); } }
這裡主要就使用者ID和IP地址進行一個正常的轉義處理,這個也是我們常規介面需要處理的一種常見的情況之一。
排序我們是以執行時間進行排序,倒序顯示即可,因此重寫排序函式。
/// <summary> /// 自定義排序處理 /// </summary> /// <param name="query"></param> /// <param name="input"></param> /// <returns></returns> protected override IQueryable<AuditLog> ApplySorting(IQueryable<AuditLog> query, AuditLogPagedDto input) { return base.ApplySorting(query, input).OrderByDescending(s => s.ExecutionTime);//時間降序 }
一般情況下,我們就基本完成了這個模組的處理了,這樣我們在介面上在花點功夫就可以呼叫這個API介面進行顯示資訊了,如下介面是我編寫的審計日誌分頁列表顯示介面。
明細展示介面如下所示。
上面列表介面管理中,如果我們還能夠以使用者進行過濾,那就更好了,因此需要新增一個使用者名稱進行過濾(注意不是使用者ID),系統表裡面沒有使用者名稱稱。
如果我們需要使用者名稱稱過濾,如下介面所示。
那麼我們就需要在應用服務層的過濾函式裡面處理相應的規則了。
我們先建立一個審計日誌和使用者資訊的集合物件,如下所示。
/// <summary> /// 審計日誌和使用者的領域物件集合 /// </summary> public class AuditLogAndUser { public AuditLog AuditLog { get;set;} public User User { get; set; } }
然後在 CreateFilteredQuery 函式裡面進行處理,如下程式碼所示。
/// <summary> /// 自定義條件處理 /// </summary> /// <param name="input">分頁查詢Dto物件</param> /// <returns></returns> protected override IQueryable<AuditLog> CreateFilteredQuery(AuditLogPagedDto input) { //構建關聯查詢Query var query = from auditLog in Repository.GetAll() join user in _userRepository.GetAll() on auditLog.UserId equals user.Id into userJoin from joinedUser in userJoin.DefaultIfEmpty() where auditLog.UserId.HasValue select new AuditLogAndUser { AuditLog = auditLog, User = joinedUser }; //過濾分頁條件 return query .WhereIf(!string.IsNullOrEmpty(input.UserName), t => t.User.UserName.Contains(input.UserName)) .WhereIf(input.ExecutionTimeStart.HasValue, s => s.AuditLog.ExecutionTime >= input.ExecutionTimeStart.Value) .WhereIf(input.ExecutionTimeEnd.HasValue, s => s.AuditLog.ExecutionTime <= input.ExecutionTimeEnd.Value) .Select(s => s.AuditLog); }
上面其實就是先通過EF的關聯表查詢,返回一個集合記錄,然後在判斷使用者名稱是否在集合裡面,最後返回所需的實體物件列表。
這個EF的關聯表查詢非常關鍵,這個也是我們聯合查詢的精髓所在,通過LINQ的方式,可以很方便實現關聯表的查詢處理並獲得對應的結果。
而對於使用者登入日誌,由於系統記錄了使用者名稱,那麼過濾使用者名稱,這不需要這麼大費周章關聯表進行處理,只需要判斷資料庫欄位對應情況即可,這種方便很多。
/// <summary> /// 自定義條件處理 /// </summary> /// <param name="input"></param> /// <returns></returns> protected override IQueryable<UserLoginAttempt> CreateFilteredQuery(UserLoginAttemptPagedDto input) { return base.CreateFilteredQuery(input) .WhereIf(!string.IsNullOrEmpty(input.UserNameOrEmailAddress), t => t.UserNameOrEmailAddress.Contains(input.UserNameOrEmailAddress)) .WhereIf(input.CreationTimeStart.HasValue, s => s.CreationTime >= input.CreationTimeStart.Value) .WhereIf(input.CreationTimeEnd.HasValue, s => s.CreationTime <= input.CreationTimeEnd.Value); }
同樣系統使用者登入日誌介面如下所示。
使用者登入明細介面效果如下所示。
以上就是對於審計日誌和使用者登入日誌的擴充套件實現,包括了對相關DTO的增加和實現應用服務層介面,以及對Web API Caller層的實現。
/// <summary> /// 審計日誌的Web API呼叫處理 /// </summary> public class AuditLogApiCaller : AsyncCrudApiCaller<AuditLogDto, long, AuditLogPagedDto>, IAuditLogAppService<AuditLogDto, long, AuditLogPagedDto> { /// <summary> /// 提供單件物件使用 /// </summary> public static AuditLogApiCaller Instance { get { return Singleton<AuditLogApiCaller>.Instance; } } /// <summary> /// 預設建構函式 /// </summary> public AuditLogApiCaller() { this.DomainName = "AuditLog";//指定域物件名稱,用於組裝介面地址 } }
由於只是部分實現功能,我們還是可以基於前面介紹開發模式(利用程式碼生成工具Database2Sharp快速生成)來實現ABP優化框架類檔案的生成,以及介面程式碼的生成,然後進行一定的調整就是本專案的程式碼了。
程式碼生成工具的ABP專案程式碼模板,和基於ABPWinform介面程式碼的模板,是我基於實際專案的反覆優化和驗證,並儘量減少冗餘程式碼而完成的一種快速開發方式,基於這樣開發方式可以大大減少專案開發的難度,提高開發效率,並完全匹配整個框架的需要,是一種非常愜意的快速開發方