前言

上次说了利用 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. 基于proteus的4026的二分频计数

    基于proteus的4026的二分频计数 1.芯片原理 4026还是一个CMOS芯片,是直接输出段码的计数器.显然,这个芯片的作用就是和七段数码管配合,直接将计数结果显示在数码管上.这里只是用于分频, ...

  2. KingbaseES V8R6 逻辑恢复到新的 schema

    前言 本文介绍一下KingbaseES V8R6版本中逻辑恢复时,将原有的对象恢复到新的schema. sys_restore命令中如果只加入了-g(原schema) -G(新schema)参数 那么 ...

  3. 性能对比 Go、Python、Perl、Ruby、Rust、C/C++、PHP、Node.js、Java.. 等多编

    1. 有人说 Python 性能没那么 Low? 这个我用 pypy 2.7 确认了下,确实没那么差, 如果用 NumPy 或 其它版本 Python 的话,性能更快.但 pypy 还不完善,pypy ...

  4. 4 PyExecJS模块

    PyExecJS模块 pyexecjs是一个可以帮助我们运行js代码的一个第三方模块. 其使用是非常容易上手的. 但是它的运行是要依赖能运行js的第三方环境的. 这里我们选择用node作为我们运行js ...

  5. #珂朵莉树#CF896C Willem, Chtholly and Seniorious

    题目 支持区间加,区间推平,询问区间第\(k\)小, 以及询问区间\(\sum{a_i^x}\pmod y\),数据随机 分析 由于数据随机,那么区间推平的概率为\(\frac{1}{4}\), 考虑 ...

  6. Java轻松实现,每天给对象发情话!

    一.引言 最近看到一篇用js代码实现表白的文章,深有感触.然后发现自己也可以用java代码实现,然后就开始写代码了,发现还挺有意思的,话不多说开搞实现思路: 使用HttpClient远程获取彩虹屁生成 ...

  7. 知识图谱增强的KG-RAG框架

    昨天我们聊到KG在RAG中如何发挥作用,今天我们来看一个具体的例子. 我们找到一篇论文: https://arxiv.org/abs/2311.17330 ,论文的研究人员开发了一种名为知识图谱增强的 ...

  8. .NET Emit 入门教程:第六部分:IL 指令:7:详解 ILGenerator 指令方法:分支条件指令

    前言: 经过前面几篇的学习,我们了解到指令的大概分类,如: 参数加载指令,该加载指令以 Ld 开头,将参数加载到栈中,以便于后续执行操作命令. 参数存储指令,其指令以 St 开头,将栈中的数据,存储到 ...

  9. linux下升级openssh参考[不建议采用此法安装]

    本人不见一采用这种方法安装,只是当遇到问题时候有一定的参考意义,所以贴了上来.建议使用yum方式安装,详见在下另一篇博文: http://blog.csdn.net/lqzixi/article/de ...

  10. AI极速批量换脸!Roop-unleashed下载介绍,可直播

    要说AI换脸领域,最开始火的项目就是Roop了,Roop-unleashed作为Roop的嫡系分支,不仅继承了前者的强大基因,更是在功能上实现了重大突破与升级 核心特性 1.可以进行高精度的图片.视频 ...