Asp-Net-Core开发笔记:进一步实现非侵入性审计日志功能
前言
上次说了利用 AOP 思想实现了审计日志功能,不过有同学反馈还是无法实现完全无侵入,于是我又重构了一版新的。
回顾一下:Asp-Net-Core开发笔记:实现动态审计日志功能
现在已经可以实现对业务代码完全无侵入的审计日志了,在需要审计的接口上加上 [AuditLog] 特性,就可以记录这个接口的操作日志,还有相关的实体变化记录,还算是方便。
PS:后面我发现 ABP 里自带审计功能,突然感觉有点了
重构
先对之前的代码进行重构,之前把跟审计有关的代码分散到各个目录中,这个功能其实是个整体,应该把代码归集到一起比较好。
创建 src/Acme.Demo/Contrib/Audit 目录 (注:Acme.Demo 是项目名称,随便起的)
目录结构
目录结构如下
 Audit
 ├─ Services
 │  ├─ IAuditLogService.cs
 │  ├─ AuditLogService.cs
 │  └─ AuditLogMongoService.cs
 ├─ Middlewares
 │  └─ AuditLogMiddleware.cs
 ├─ Filters
 │  └─ AuditLogAttribute.cs
 ├─ Extensions
 │  └─ CfgAudit.cs
 ├─ EventHandlers
 │  └─ FreeSqlAuditEventHandler.cs
 ├─ Entities
 │  ├─ EntityChangeInfo.cs
 │  └─ AuditLog.cs
 └─ AuditConstant.cs
6 directories, 10 files
创建 EntityChangeInfo 实体
用来保存实体变化
public class EntityChangeInfo {
  public string Entity { get; set; }
  public string Action { get; set; }
  public string Sql { get; set; }
  public Dictionary<string, object?> Parameters { get; set; }
}
AuditLog重构
之前我们是把实体变化内容直接保存在 AuditLog 里
现在要分离开,使用 List<EntityChangeInfo> 类型的 EntityChanges 属性来存放实体变化
public class AuditLog {
  /// <summary>
  /// 事件唯一标识
  /// </summary>
  public string EventId { get; set; }
  /// <summary>
  /// 事件类型(例如:登录、登出、数据修改等)
  /// </summary>
  public string EventType { get; set; }
  /// <summary>
  /// 执行操作的用户标识
  /// </summary>
  public string UserId { get; set; }
  /// <summary>
  /// 执行操作的用户名
  /// </summary>
  public string Username { get; set; }
  /// <summary>
  /// 事件发生的时间戳
  /// </summary>
  public DateTime Timestamp { get; set; }
  /// <summary>
  /// 用户的IP地址
  /// </summary>
  public string? IPAddress { get; set; }
  /// <summary>
  /// 实体更改内容,可根据实际情况以JSON格式存储
  /// </summary>
  public List<EntityChangeInfo>? EntityChanges { get; set; } = new();
  /// <summary>
  /// 路由信息
  /// </summary>
  public Dictionary<string, object?> RouteData { get; set; }
  /// <summary>
  /// 事件描述
  /// </summary>
  public string? Description { get; set; }
  /// <summary>
  /// 额外信息 (考虑以 JSON 格式保存)
  /// </summary>
  public object? Extra { get; set; }
  /// <summary>
  /// 创建时间
  /// </summary>
  public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
  /// <summary>
  /// 修改时间
  /// </summary>
  public DateTime ModifiedTime { get; set; } = DateTime.UtcNow;
}
过滤器重构
修改 AuditLogAttribute 类
涉及到的改动不多,就是简化了参数,只需要传入 EventType 就行
其他的都会自动获取
实体变化部分,需要使用到 ORM 的功能,接下来会介绍
public class AuditLogAttribute : ActionFilterAttribute {
  public string EventType { get; set; }
  public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) {
    var sp = context.HttpContext.RequestServices;
    var ctxItems = context.HttpContext.Items;
    try {
      var authService = sp.GetRequiredService<AuthService>();
      // 在操作执行前
      var executedContext = await next();
      // 在操作执行后
      // 获取当前用户的身份信息
      var user = await authService.GetUserFromJwt(executedContext.HttpContext.User);
      // 构造AuditLog对象
      var auditLog = new AuditLog {
        EventId = Guid.NewGuid().ToString(),
        EventType = this.EventType,
        UserId = user.UserId,
        Username = user.Username,
        Timestamp = DateTime.UtcNow,
        IPAddress = GetIpAddress(executedContext.HttpContext),
        Description = $"操作类型:{this.EventType}",
      };
      if (ctxItems.TryGetValue(AuditConstant.EntityChanges, out var item)) {
        auditLog.EntityChanges = item as List<EntityChangeInfo>;
      }
      var routeData = new Dictionary<string, object?>();
      foreach (var (key, value) in context.RouteData.Values) {
        routeData.Add(key, value);
      }
      auditLog.RouteData = routeData;
      var auditService = sp.GetRequiredService<IAuditLogService>();
      await auditService.LogAsync(auditLog);
    } catch (Exception ex) {
      var logger = sp.GetRequiredService<ILogger<AuditLogAttribute>>();
      logger.LogError(ex, "An error occurred while logging audit information.");
    }
    Console.WriteLine(
      "执行 AuditLogAttribute, " +
      $"EventId: {ctxItems["AuditLog_EventId"]}");
  }
  private string? GetIpAddress(HttpContext httpContext) {
    // 首先检查X-Forwarded-For头(当应用部署在代理后面时)
    var forwardedFor = httpContext.Request.Headers["X-Forwarded-For"].FirstOrDefault();
    if (!string.IsNullOrWhiteSpace(forwardedFor)) {
      return forwardedFor.Split(',').FirstOrDefault(); // 可能包含多个IP地址
    }
    // 如果没有X-Forwarded-For头,或者需要直接获取连接的远程IP地址
    return httpContext.Connection.RemoteIpAddress?.ToString();
  }
}
获取实体变化
实体变化部分,需要使用到 ORM 的功能,不同的 ORM 能实现的实体变化监控不太一样,需要每种 ORM 写一个
我目前只实现了 FreeSQL 的实体变化监控
代码在 FreeSqlAuditEventHandler 中
public class FreeSqlAuditEventHandler {
  private readonly ILogger<FreeSqlAuditEventHandler> _logger;
  private readonly IHttpContextAccessor _httpContextAccessor;
  private readonly IDictionary<object, object?> _ctxItems;
  public FreeSqlAuditEventHandler(IHttpContextAccessor httpContextAccessor,
                                  ILogger<FreeSqlAuditEventHandler> logger) {
    _httpContextAccessor = httpContextAccessor;
    _logger = logger;
    _ctxItems = httpContextAccessor.HttpContext?.Items ?? new Dictionary<object, object?>();
  }
  public void HandleCurdBefore(object? sender, CurdBeforeEventArgs args) {
    // 捕获变更信息
    var changeInfo = new EntityChangeInfo {
      Entity = args.EntityType.Name,
      Action = Enum.GetName(typeof(CurdType), args.CurdType) ?? "unknown",
      Sql = args.Sql,
      Parameters = new Dictionary<string, object?>(
        args.DbParms.Select(p => new KeyValuePair<string, object?>(p.ParameterName, p.Value))
      )
    };
    // 处理CurdBefore事件,将实体变化信息保存到HttpContext.Items
    _logger.LogDebug(
      $"执行 FreeSql CurdBefore, " +
      $"EventId: {_httpContextAccessor.HttpContext?.Items["AuditLog_EventId"]}, " +
      $"entityType: {args.EntityType.Name}, " +
      $"crud: {Enum.GetName(typeof(CurdType), args.CurdType)}, ");
    List<EntityChangeInfo> changes = new();
    if (_ctxItems.TryGetValue(AuditConstant.EntityChanges, out var item)) {
      changes = item as List<EntityChangeInfo> ?? new List<EntityChangeInfo>();
    } else {
      _ctxItems[AuditConstant.EntityChanges] = changes;
    }
    changes.Add(changeInfo);
  }
}
这里很简单,利用 FreeSQL 的 Aop.CurdBefore 事件,把 HandleCurdBefore 绑定到事件上,就可以获取实体的变化了。
// 创建 IFreeSQL 实例
IFreeSql inst = ...;
// 实体 CRUD操作(create read update delete)事件
inst.Aop.CurdBefore += auditEventHandler.HandleCurdBefore;
这里吐槽一下 FreeSQL 的命名,一般都叫 crud ,你却搞特殊变成 curd ……
不过为了用国产数据库,只能凑合用咯~
扩展方法
为了使用方便
我把注册服务和中间件都放在扩展方法中,符合 AspNetCore 的开发习惯
public static class CfgAudit {
  public static IServiceCollection AddAudit(this IServiceCollection services, IConfiguration conf) {
    services.AddSingleton<IAuditLogService>(sp =>
                                            new AuditLogMongoService(conf.GetConnectionString("MongoDB"), "stu_data_hub"));
    services.AddSingleton<FreeSqlAuditEventHandler>();
    return services;
  }
  public static IApplicationBuilder UseAudit(this IApplicationBuilder app) {
    app.UseMiddleware<AuditLogMiddleware>();
    return app;
  }
}
在 Program.cs 里注册
// 注册服务
builder.Services.AddAudit(builder.Configuration);
// 添加中间件
app.UseAudit();
PS:这里把配置传进去有点蠢,其实我完全可以在 AddAudit 方法里通过依赖注入的方式来获取配置对象的,不过既然都这样写了,懒得改了。
使用效果
来看下使用效果
首先在需要审计的接口上加上 [AuditLog] 特性
/// <summary>
/// 设置反馈结果
/// </summary>
[AuditLog(EventType = "设置反馈结果")]
[HttpPost("{taskId}/sub-tasks/{subId}/set-feedback")]
public async Task<ApiResponse> SetSubTaskFeedback(string taskId, string subId, [FromBody] SubTaskFeedbackDto dto) {}
之后在 MongoDB 里可以看到审计日志(数据已脱敏)
{
  "_id": {
    "$oid": "65ff019f6de4b7290e1da9e9"
  },
  "EventId": "eb81f052-ce84-4923-bf9e-57582e464992",
  "EventType": "设置反馈结果",
  "UserId": "eb81f052",
  "Username": "用户名",
  "Timestamp": {
    "$date": "2024-03-23T16:21:49.697Z"
  },
  "IPAddress": "1.2.3.4",
  "EntityChanges": [
    {
      "Entity": "实体名称",
      "Action": "Select",
      "Sql": "Select 语句已脱敏",
      "Parameters": {}
    },
    {
      "Entity": "实体名称",
      "Action": "Update",
      "Sql": "UPDATE entity set some_col=:p_0",
      "Parameters": {
        ":p_0": 6
      }
    }
  ],
  "RouteData": {
    "area": "Market",
    "action": "SetSubTaskFeedback",
    "controller": "Task",
    "taskId": "eb81f052",
    "subId": "57582e464992"
  },
  "Description": "操作类型:设置反馈结果",
  "Extra": null,
  "CreatedTime": {
    "$date": "2024-03-23T16:21:49.697Z"
  },
  "ModifiedTime": {
    "$date": "2024-03-23T16:21:49.697Z"
  }
}
可以看到 EntityChanges 字段包含了这次事件中的实体操作,也就是对数据库的操作,共有两个,一个是 select 查询,另一个是 update 修改数据库。
AuditLog 中间件
最后说下这个  AuditLogMiddleware
代码很简单,就是在每个请求进来的时候,在 HttpContext.Items 里添加一个 AuditConstant.EventId
public class AuditLogMiddleware {
  private readonly RequestDelegate _next;
  public AuditLogMiddleware(RequestDelegate next) {
    _next = next;
  }
  public async Task Invoke(HttpContext context) {
    // 生成 EventId 并存储到 HttpContext.Items 中
    context.Items[AuditConstant.EventId] = Guid.NewGuid().ToString();
    await _next(context);
  }
}
虽然写了这个中间件,不过后面并没有用上这个 EventId
这个本来是用来把实体更新和 Filter 关系起来的,不过后面发现用不上。
先留着吧,万一后面有用呢?
Asp-Net-Core开发笔记:进一步实现非侵入性审计日志功能的更多相关文章
- Asp.Net Core中利用Seq组件展示结构化日志功能
		
在一次.Net Core小项目的开发中,掌握的不够深入,对日志记录并没有好好利用,以至于一出现异常问题,都得跑动服务器上查看,那时一度怀疑自己肯定没学好,不然这一块日志不可能需要自己扒服务器日志来查看 ...
 - 在CentOS7 开发与部署 asp.net core app笔记
		
原文:在CentOS7 开发与部署 asp.net core app笔记 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/lihongzhai/art ...
 - C# -- HttpWebRequest 和 HttpWebResponse 的使用  C#编写扫雷游戏  使用IIS调试ASP.NET网站程序  WCF入门教程  ASP.Net Core开发(踩坑)指南   ASP.Net Core Razor+AdminLTE 小试牛刀  webservice创建、部署和调用  .net接收post请求并把数据转为字典格式
		
C# -- HttpWebRequest 和 HttpWebResponse 的使用 C# -- HttpWebRequest 和 HttpWebResponse 的使用 结合使用HttpWebReq ...
 - 2月送书福利:ASP.NET Core开发实战
		
大家都知道我有一个公众号“恰童鞋骚年”,在公众号2020年第一天发布的推文<2020年,请让我重新介绍我自己>中,我曾说到我会在2020年中每个月为所有关注“恰童鞋骚年”公众号的童鞋们送一 ...
 - [转]ASP.NET Core 开发-Logging 使用NLog 写日志文件
		
本文转自:http://www.cnblogs.com/Leo_wl/p/5561812.html ASP.NET Core 开发-Logging 使用NLog 写日志文件. NLog 可以适用于 . ...
 - ASP.NET Core 开发-中间件(Middleware)
		
ASP.NET Core开发,开发并使用中间件(Middleware). 中间件是被组装成一个应用程序管道来处理请求和响应的软件组件. 每个组件选择是否传递给管道中的下一个组件的请求,并能之前和下一组 ...
 - ASP.NET Core开发-Docker部署运行
		
ASP.NET Core开发Docker部署,.NET Core支持Docker 部署运行.我们将ASP.NET Core 部署在Docker 上运行. 大家可能都见识过Docker ,今天我们就详细 ...
 - ASP.NET Core开发-后台任务利器Hangfire使用
		
ASP.NET Core开发系列之后台任务利器Hangfire 使用. Hangfire 是一款强大的.NET开源后台任务利器,无需Windows服务/任务计划程序. 可以使用于ASP.NET 应用也 ...
 - ASP.NET Core开发-读取配置文件Configuration
		
ASP.NET Core 是如何读取配置文件,今天我们来学习. ASP.NET Core的配置系统已经和之前版本的ASP.NET有所不同了,之前是依赖于System.Configuration和XML ...
 - ASP.NET Core 开发-Entity Framework (EF) Core 1.0 Database First
		
ASP.NET Core 开发-Entity Framework Core 1.0 Database First,ASP.NET Core 1.0 EF Core操作数据库. Entity Frame ...
 
随机推荐
- Redis源码学习(1)──字符串
			
redis 版本:5.0 本文代码在Redis源码中的位置:redis/src/sds.c.redis/src/sds.h 源码整体结构 src:核心实现代码,用 C 语言编写 tests:单元测试代 ...
 - KingbaseES V8R3 集群运维案例 -- cluster.log无日志输出问题诊断
			
案例说明: KingbaseES V8R3集群正常运行期间,现场发现cluster.log日志无任何信息输出,针对这一问题做了复现及提出解决方案.后现场检查发现,cluster.log文件曾被删除: ...
 - OPC报文详解
			
OPC (OLE for Process Control) 是一种工业通讯协议的标准,用于实现不同制造商的设备和系统之间的数据交换.它主要用于工业自动化系统中.OPC标准有几个不同的规范,包括OPC ...
 - Windows配置Git本地仓库
			
git版本控制常用命令 1.配置身份信息 git config --global user.name "ycw.42624" # 名称 git config --global us ...
 - MySQL创建和操纵表
			
表创建基础 CREATE TABLE customers ( cust_id int NOT NULL AUTO_INCREMENT , cust_name char(50) NOT NULL , c ...
 - 3个.NET开源、免费、强大的商城系统
			
前言 今天大姚给大家分享3个.NET开源.免费.强大的商城系统,希望可以帮助到有商城系统开发需求的同学. nopCommerce nopCommerce是一个功能丰富.免费.灵活且可定制的开源电子商务 ...
 - #轮廓线dp,博弈论#洛谷 4363 [九省联考 2018] 一双木棋 chess
			
题目传送门 分析 菲菲想让答案尽量大,牛牛想让答案尽量小. 很天真的一种想法就是设 \(dp[i][j]\) 表示现在选择 \((i,j)\) 的答案. 但是这样有一个弊端就是并不知道其它位置怎么选择 ...
 - #博弈论#Poj 2484 A Funny Game
			
题目 \(n\)个石子排成一圈,每次可以取一个或相邻的一对, 取完为胜,问先手是否必胜 分析 无论先手如何取,后手都能模仿先手的取法. 比如说,当石子个数为奇数时先手取相邻的一对,后手可以将对面的那一 ...
 - DevEco Device Tool 助力OpenHarmony设备开发
			
DevEco Device Tool 为设备开发者提供一站式的开发环境和资源获取通道,实现了从芯片模板工程创建.到开发资源挑选定制,再到快速编码.轻小型系统调试调优.烧录环节的全流程覆盖,帮助开发者实 ...
 - CMake 入门教程:从零开始构建 C/C++ 项目
			
CMake是一个跨平台的自动化构建工具,可以用于构建各种类型的项目,包括*C++.C.Python.Java*等.本文将从零开始,介绍如何使用CMake构建一个简单的C/C++项目 安装CMake 首 ...