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

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

1、审计日志和登录日志的基础

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

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

我们查看系统数据库,可以看到对应这两个部分的日志表,如下所示。

image

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

image

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

image

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

image

这些也就代表它们都有底层的实现,但是没有服务层应用和DTO对象,因此我们需要扩展这些内容才能够管理显示这些记录信息。

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

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

image

或者屏蔽某些接口

image

另外,如果我们不想公布某些特殊的接口访问,那么我们可以通过标记 [RemoteService(false)] 进行屏蔽,这样在Web API层就不会公布对应的接口了。

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

image

2、系统审计日志和登录日志的完善

前面介绍了,审计日志和登陆日志的处理,Abp系统只是做了一部分底层的内容,我们如果进行这些信息的管理,我们需要完善它,增加对应的DTO类和应用服务层接口和接口实现。

首先我们根据底层的领域实体对象的属性,复制过来作为对应DTO对象的属性,并增加对应的分页条件DTO对象,由于我们不需要进行创建,因此不需要增加Create***Dto对象类。

如对于审计日志的DTO对象,我们定义如下所示(主要复制领域对象的属性)。

image

而分页处理的DTO对象如下所示,我们主要增加一个用户名和创建时间区间的条件。

image

对于登录日志的DTO对象,我们依葫芦画瓢,也是如此操作即可。

image

登录日志的分页对象Dto如下所示、

image

完善了这些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接口进行显示信息了,如下界面是我编写的审计日志分页列表显示界面。

image

明细展示界面如下所示。

image

上面列表界面管理中,如果我们还能够以用户进行过滤,那就更好了,因此需要添加一个用户名进行过滤(注意不是用户ID),系统表里面没有用户名称。

image

如果我们需要用户名称过滤,如下界面所示。

image

那么我们就需要在应用服务层的过滤函数里面处理相应的规则了。

我们先创建一个审计日志和用户信息的集合对象,如下所示。

    /// <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);
        }

同样系统用户登录日志界面如下所示。

image

用户登录明细界面效果如下所示。

image

以上就是对于审计日志和用户登录日志的扩展实现,包括了对相关DTO的增加和实现应用服务层接口,以及对Web API Caller层的实现。

image
    /// <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优化框架类文件的生成,以及界面代码的生成,然后进行一定的调整就是本项目的代码了。

image

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容