9.22 审计日志

9.22.1 审计日志#

在一个企业应用系统中,用户对系统所有的操作包括请求,数据库操作等等都应该记录起来,那么这些日志我们成为操作日志,也可以说审计日志。

通常来说,我们审计日志更多指的是数据库的操作记录。

审计日志一般会记录以下三个操作:

  • 新增操作:记录某某人在某某时间对哪个表新增了什么数据
  • 更新操作:记录某某人在某某时间对哪个表的哪些数据做了更改,记录更改前的值和更改后的值
  • 删除操作:记录某某人在某某时间对哪个表删除了什么数据

9.22.2 关于 SaveChanges 事件#

Furion 框架中为所有 AppDbContext 子类都提供了三个可重写的方法,这三个方法分别由三个事件触发:

  • 提交更改之前 SavingChanges 事件:触发 void SavingChangesEvent(DbContextEventData eventData, InterceptionResult<int> result) 方法
  • 提交更改之后 SavedChanges 事件:触发 void SavedChangesEvent(SaveChangesCompletedEventData eventData, int result) 方法
  • 提交更改失败 SaveChangesFailed 事件:触发 void SaveChangesFailedEvent(DbContextErrorEventData eventData) 方法

通过这三个事件我们就可以捕获所有更改的实体然后保存到数据库审计日志中。

9.22.3 如何使用#

我们只需要在 AppDbContext 子类中重写 SavingChanges 事件对应方法即可:

using Furion.DatabaseAccessor;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using System;
using System.Linq;
using System.Security.AccessControl;
namespace Furion.EntityFramework.Core
{
[AppDbContext("Sqlite3ConnectionString")]
public class FurDbContext : AppDbContext<FurDbContext>
{
public FurDbContext(DbContextOptions<FurDbContext> options) : base(options)
{
}
/// <summary>
/// 重写保存之前事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected override void SavingChangesEvent(DbContextEventData eventData, InterceptionResult<int> result)
{
// 获取当前事件对应上下文
var dbContext = eventData.Context;
// 强制重新检查一边实体更改信息
// dbContext.ChangeTracker.DetectChanges();
// 获取所有更改,删除,新增的实体,但排除审计实体(避免死循环)
var entities = dbContext.ChangeTracker.Entries()
.Where(u => u.Entity.GetType() != typeof(Audit) && (u.State == EntityState.Modified || u.State == EntityState.Deleted || u.State == EntityState.Added))
.ToList();
// 通过请求中获取当前操作人
var userId = App.GetService<IHttpContextAccessor>().HttpContext.Items["UserId"];
// 获取所有已更改的实体
foreach (var entity in entities)
{
// 获取实体类型
var entityType = entity.Entity.GetType();
// 获取所有实体有效属性,排除 [NotMapper] 属性
var props = entity.OriginalValues.Properties;
// 获取实体当前(现在)的值
var currentValues = entity.CurrentValues;
// 获取数据库中实体的值
var databaseValues = entity.GetDatabaseValues();
// 遍历所有属性
foreach (var prop in props)
{
// 获取属性名
var propName = prop.Name;
// 获取现在的实体值
var newValue = currentValues[propName];
object oldValue = null;
// 如果是新增数据,则 databaseValues 为空,所以需要判断一下
if (databaseValues != null)
{
oldValue = databaseValues[propName];
}
// 插入审计日志表
dbContext.Audits.Add(new Audit
{
Table = entityType.Name, // 表名
Column = propName, // 更新的列
NewValue = newValue, // 新值
OldValue = oldValue, // 旧值
CreatedTime = DateTime.Now, // 操作时间
UserId = userId, // 操作人
Operate = entity.State.ToString(); // 操作方式:新增、更新、删除
});
}
}
}
}
}
小知识

如果对性能有所要求,那么建议审计日志通过 日志组件 写入数据库,如,通过 Nlog、Log4Net 这些等:

// 插入审计日志表
dbContext.Audits.Add(new Audit
{
Table = entityType.Name, // 表名
Column = propName, // 更新的列
newValue = newValue, // 新值
OldValue = oldValue, // 旧值
CreatedTime = DateTime.Now, // 操作时间
UserId = userId, // 操作人
Operate = entity.State.ToString(); // 操作方式:新增、更新、删除
});

替换为:

logger.Information(JsonConvert.SerializeObject(new Audit
{
Table = entityType.Name, // 表名
Column = propName, // 更新的列
newValue = newValue, // 新值
OldValue = oldValue, // 旧值
CreatedTime = DateTime.Now, // 操作时间
UserId = userId, // 操作人
Operate = entity.State.ToString(); // 操作方式:新增、更新、删除
}));

通过上面的例子,我们就可以对数据库所有的新增、更新、删除进行监控了。

9.22.4 反馈与建议#

与我们交流

给 Furion 提 Issue