前言

上次说了利用 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开发笔记:进一步实现非侵入性审计日志功能的更多相关文章

  1. Asp.Net Core中利用Seq组件展示结构化日志功能

    在一次.Net Core小项目的开发中,掌握的不够深入,对日志记录并没有好好利用,以至于一出现异常问题,都得跑动服务器上查看,那时一度怀疑自己肯定没学好,不然这一块日志不可能需要自己扒服务器日志来查看 ...

  2. 在CentOS7 开发与部署 asp.net core app笔记

    原文:在CentOS7 开发与部署 asp.net core app笔记 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/lihongzhai/art ...

  3. 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 ...

  4. 2月送书福利:ASP.NET Core开发实战

    大家都知道我有一个公众号“恰童鞋骚年”,在公众号2020年第一天发布的推文<2020年,请让我重新介绍我自己>中,我曾说到我会在2020年中每个月为所有关注“恰童鞋骚年”公众号的童鞋们送一 ...

  5. [转]ASP.NET Core 开发-Logging 使用NLog 写日志文件

    本文转自:http://www.cnblogs.com/Leo_wl/p/5561812.html ASP.NET Core 开发-Logging 使用NLog 写日志文件. NLog 可以适用于 . ...

  6. ASP.NET Core 开发-中间件(Middleware)

    ASP.NET Core开发,开发并使用中间件(Middleware). 中间件是被组装成一个应用程序管道来处理请求和响应的软件组件. 每个组件选择是否传递给管道中的下一个组件的请求,并能之前和下一组 ...

  7. ASP.NET Core开发-Docker部署运行

    ASP.NET Core开发Docker部署,.NET Core支持Docker 部署运行.我们将ASP.NET Core 部署在Docker 上运行. 大家可能都见识过Docker ,今天我们就详细 ...

  8. ASP.NET Core开发-后台任务利器Hangfire使用

    ASP.NET Core开发系列之后台任务利器Hangfire 使用. Hangfire 是一款强大的.NET开源后台任务利器,无需Windows服务/任务计划程序. 可以使用于ASP.NET 应用也 ...

  9. ASP.NET Core开发-读取配置文件Configuration

    ASP.NET Core 是如何读取配置文件,今天我们来学习. ASP.NET Core的配置系统已经和之前版本的ASP.NET有所不同了,之前是依赖于System.Configuration和XML ...

  10. 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 ...

随机推荐

  1. KingbaseES 行级安全策略介绍

    本文详细介绍了KingbaseES中通过CREATE POLICY为一个表定义一条行级安全性策略.注意为了应用已被创建的策略,在表上必须启用行级安全性. 策略名称是针对每个表的.因此,一个策略名称可以 ...

  2. OGNL表达式注入分析

    OGNL基础 依赖 <dependency> <groupId>ognl</groupId> <artifactId>ognl</artifact ...

  3. 实现基于TCP的服务端/客户端

    服务端套接字创建过程 第一步:调用socket函数创建套接字 //成功时返回文件表述符,失败时返回-1 int socket(int __domain, int __type, int __proto ...

  4. 学习Source Generators之从swagger中生成类

    前面学习了一些Source Generators的基础只是,接下来就来实践一下,用这个来生成我们所需要的代码. 本文将通过读取swagger.json的内容,解析并生成对应的请求响应类的代码. 创建项 ...

  5. SQL 递归核心思想(递归思维)

    目前很缺递归思维,主要是算法代码写得少,本篇记录下以 PostgreSQL 代码举例(主要是非常喜欢这款性能小钢炮数据库). 树状查询不多说,很简单大家基本都会,主要讲 cte 代码递归实现不同需求. ...

  6. 直播预告丨Hello HarmonyOS进阶课程第四课——ArkUI动画开发

    为了帮助初识HarmonyOS的开发者快速入门,我们曾推出Hello HarmonyOS系列课程,从最基础的配置IDE和创建Hello World开始,详细介绍HarmonyOS基础.开发环境搭建.I ...

  7. js es6 delete

    前言 首先delete 不同于nodejs delete,看下有什么不同. 正文 var test=5; delete test; console.log(test); 结果是test没有受到任何影响 ...

  8. em/px/rem/vh/vw的区别

    一.介绍 传统的项目开发中,我们只会用到px.%.em这几个单位,它可以适用于大部分的项目开发,且拥有比较良好的兼容性 从CSS3开始,浏览器对计量单位的支持又提升到了另外一个境界,新增了rem.vh ...

  9. 如何快速实现Prometheus监控Kubernetes集群

    Prometheus K8S集群中常见的监控工具有哪些: Kubernetes Dashboard Kube-monkey K8s-testsuite Kubespray Minikube Prome ...

  10. Causal Inference理论学习篇-Tree Based-Causal Forest

    广义随机森林 了解causal forest之前,需要先了解其forest实现的载体:GENERALIZED RANDOM FORESTS[6](GRF) 其是随机森林的一种推广, 经典的随机森林只能 ...