我们了解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界面代码的模板,是我基于实际项目的反复优化和验证,并尽量减少冗余代码而完成的一种快速开发方式,基于这样开发方式可以大大减少项目开发的难度,提高开发效率,并完全匹配整个框架的需要,是一种非常惬意的快速开发方式。