我們了解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界面代碼的模板,是我基於實際項目的反復優化和驗證,並盡量減少冗余代碼而完成的一種快速開發方式,基於這樣開發方式可以大大減少項目開發的難度,提高開發效率,並完全匹配整個框架的需要,是一種非常愜意的快速開發方式。