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界面代码的模板,是我基于实际项目的反复优化和验证,并尽量减少冗余代码而完成的一种快速开发方式,基于这样开发方式可以大大减少项目开发的难度,提高开发效率,并完全匹配整个框架的需要,是一种非常惬意的快速开发方式。
ABP开发框架前后端开发系列---(7)系统审计日志和登录日志的管理的更多相关文章
- ABP开发框架前后端开发系列---(5)Web API调用类在Winform项目中的使用
在前面几篇随笔介绍了我对ABP框架的改造,包括对ABP总体的介绍,以及对各个业务分层的简化,Web API 客户端封装层的设计,使得我们基于ABP框架的整体方案越来越清晰化, 也越来越接近实际的项目开 ...
- ABP开发框架前后端开发系列---(4)Web API调用类的封装和使用
在前面随笔介绍ABP应用框架的项目组织情况,以及项目中领域层各个类代码组织,以及简化了ABP框架的各个层的内容,使得我们项目结构更加清晰.上篇随笔已经介绍了字典模块中应用服务层接口的实现情况,并且通过 ...
- ABP开发框架前后端开发系列---(11)菜单的动态管理
在前面随笔<ABP开发框架前后端开发系列---(9)ABP框架的权限控制管理>中介绍了基于ABP框架服务构建的Winform客户端,客户端通过Web API调用的方式进行获取数据,从而实现 ...
- ABP开发框架前后端开发系列---(9)ABP框架的权限控制管理
在前面两篇随笔<ABP开发框架前后端开发系列---(7)系统审计日志和登录日志的管理>和<ABP开发框架前后端开发系列---(8)ABP框架之Winform界面的开发过程>开始 ...
- ABP开发框架前后端开发系列---(8)ABP框架之Winform界面的开发过程
在前面随笔介绍的<ABP开发框架前后端开发系列---(7)系统审计日志和登录日志的管理>里面,介绍了如何改进和完善审计日志和登录日志的应用服务端和Winform客户端,由于篇幅限制,没有进 ...
- ABP开发框架前后端开发系列---(12)配置模块的管理
一般来说,一个系统或多或少都会涉及到一些系统参数或者用户信息的配置,而ABP框架也提供了一套配置信息的管理模块,ABP框架的配置信息,必须提前定义好配置的各项内容,然后才能在系统中初始化或者通过接口查 ...
- ABP开发框架前后端开发系列---(14)基于Winform的ABP快速开发框架
前面介绍了很多ABP系列的文章,一步一步的把我们日常开发中涉及到的Web API服务构建.登录日志和操作审计日志.字典管理模块.省份城市的信息维护.权限管理模块中的组织机构.用户.角色.权限.菜单等内 ...
- ABP开发框架前后端开发系列---(16)ABP框架升级最新版本的经验总结
有一小段时间没有持续升级ABP框架了,最近就因应客户的需要,把ABP框架进行全面的更新,由于我们应用的ABP框架,基础部分还是会使用官方的内容,因此升级的时候需要把官方基础ABP的DLL进行全面的更新 ...
- ABP开发框架前后端开发系列---(3)框架的分层和文件组织
在前面随笔<ABP开发框架前后端开发系列---(2)框架的初步介绍>中,我介绍了ABP应用框架的项目组织情况,以及项目中领域层各个类代码组织,以便基于数据库应用的简化处理.本篇随笔进一步对 ...
- ABP开发框架前后端开发系列---(2)框架的初步介绍
在前面随笔<ABP开发框架前后端开发系列---(1)框架的总体介绍>大概介绍了这个ABP框架的主要特点,以及介绍了我对这框架的Web API应用优先的一些看法,本篇继续探讨ABP框架的初步 ...
随机推荐
- uwp - 控件精确移动动画
原文:uwp - 控件精确移动动画 先看效果图: 一共有8个GRID,黄色的负责移动,其他7个负责定位.新建一个页面page,替换默认代码: <UserControl.Resources> ...
- Windows10的Ubuntu子系统开启桌面环境
原文:Windows10的Ubuntu子系统开启桌面环境 Ubuntu 优势之一就是桌面环境比较好,所以咱们的子系统当然也不能少了这一环节,本小结开始安装Ubuntu 桌面系统. 安装环境 使用下面指 ...
- svm资料收集
向量点乘(内积)和叉乘(外积.向量积)概念及几何意义解读: https://blog.csdn.net/dcrmg/article/details/52416832 三角形余弦定理:https://z ...
- js到字符串数组,实现阵列成一个字符串
数组字符串(阵列元件与字符串连接) var a, b; a = new Array(0,1,2,3,4); b = a.join("-"); 字符串转数组(根据一个字符串被分成 ...
- C/C++回调方式系列之一 函数指针和函数回调模式
一.函数指针 1. 函数的定义 return_type function_name(parameter list) { function_body } return_type: 返回值,函数一定有返回 ...
- js,css引用顺序设定
遇到的困难 在ASP .NET MVC里面,会使用_Layout.cshtml来绘制一些全局的公共页面,以及引用相关的css和js而在每个独立的页面中,也有自己独立的js一般来说,希望公共的js放在独 ...
- WPF控件深拷贝:序列化/反序列化
原文:WPF控件深拷贝:序列化/反序列化 今天DebugLZQ在做WPF拖动总结的时候,遇到了这个问题.baidu了下,貌似没有解决这个问题的权威答案,遂写下这篇博文. 我想做的事情是:拖动一个窗体内 ...
- jquery 克隆div 复制div 克隆元素 复制元素
代码: $('.div1').clone() 定义和用法 clone() 方法生成被选元素的副本,包含子节点.文本和属性. 语法 $(selector).clone(includeEvents) 参数 ...
- Linux ssh密钥自动登录 专题
在开发中,经常需要从一台主机ssh登陆到另一台主机去,每次都需要输一次login/Password,很繁琐.使用密钥登陆就可以不用输入用户名和密码了 实现从主机A免密码登陆到主机B(即把主机A的pub ...
- swift 如何控制view的显示与隐藏
swift 如何控制view的显示与隐藏 UIView有一个属性 hidden let line: UILabel = UILabel() 默认是显示的 需要显示它的时候:line.hidden = ...