ABP开发框架先后端开发系列---(7)系统审计日志和登陆日志的管理

咱们了解ABP框架内部自动记录审计日志和登陆日志的,可是这些信息只是在相关的内部接口里面进行记录,并无一个管理界面供咱们了解,可是其系统数据库记录了这些数据信息,咱们能够为它们设计一个查看和导出这些审计日志和登陆日志的管理界面。本篇随笔继续ABP框架的系列介绍,一步步深刻了解ABP框架的应用开发,介绍审计日志和登陆日志的管理。数据库

一、审计日志和登陆日志的基础

审计日志,设置咱们在访问或者调用某个应用服务层接口的时候,横切面流下的一系列操做记录,其中记录咱们访问的服务接口,参数,客户端IP地址,访问时间,以及异常等信息,这些操做都是在ABP系统自动记录的,若是咱们须要屏蔽某些服务类或者接口,则这些就不会记录在里面,不然默认是记录的。框架

登陆日志,这个就是用户尝试登陆的时候,留下的记录信息,其中包括用户的登陆用户名,ID,IP地址、登陆时间,以及登陆是否成功的状态等信息。ide

咱们查看系统数据库,能够看到对应这两个部分的日志表,以下所示。函数

在ABP框架内部基础项目Abp里面,咱们能够看到对应的领域对象实体和Store管理类,不过并无在应用层的对应服务和相关的DTO,咱们须要实现一个审计日志和登录日志的管理功能界面,界面效果以下所示。工具

 咱们搜索ABP项目,查找到审计日志的相关类(包含领域对象实体和Store管理类),以下界面截图。优化

一样对于系统登陆日志对象,咱们查找到对应的领域实体和对应的Manger业务逻辑类。this

这些也就表明它们都有底层的实现,可是没有服务层应用和DTO对象,所以咱们须要扩展这些内容才可以管理显示这些记录信息。spa

前面介绍过,默认的通常应用服务层和接口,都是会进行审计记录写入的,若是咱们须要屏蔽某些应用服务层或者接口,不进行审计信息的记录,那么须要使用特性标记[DisableAuditing]来管理。设计

如咱们针对审计日志应用层接口的访问,咱们不想让它多余的记录,那么就设置这个标记便可。3d

或者屏蔽某些接口

另外,若是咱们不想公布某些特殊的接口访问,那么咱们能够经过标记 [RemoteService(false)]  进行屏蔽,这样在Web API层就不会公布对应的接口了。

如对于审计日志的记录,增删改咱们都不容许客户端进行操做,那么咱们把对应的应用服务层接口屏蔽便可。

 

二、系统审计日志和登陆日志的完善

前面介绍了,审计日志和登录日志的处理,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界面代码的模板,是我基于实际项目的反复优化和验证,并尽可能减小冗余代码而完成的一种快速开发方式,基于这样开发方式能够大大减小项目开发的难度,提升开发效率,并彻底匹配整个框架的须要,是一种很是惬意的快速开发方式。