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 ...
随机推荐
- 基于proteus的4026的二分频计数
基于proteus的4026的二分频计数 1.芯片原理 4026还是一个CMOS芯片,是直接输出段码的计数器.显然,这个芯片的作用就是和七段数码管配合,直接将计数结果显示在数码管上.这里只是用于分频, ...
- KingbaseES V8R6 逻辑恢复到新的 schema
前言 本文介绍一下KingbaseES V8R6版本中逻辑恢复时,将原有的对象恢复到新的schema. sys_restore命令中如果只加入了-g(原schema) -G(新schema)参数 那么 ...
- 性能对比 Go、Python、Perl、Ruby、Rust、C/C++、PHP、Node.js、Java.. 等多编
1. 有人说 Python 性能没那么 Low? 这个我用 pypy 2.7 确认了下,确实没那么差, 如果用 NumPy 或 其它版本 Python 的话,性能更快.但 pypy 还不完善,pypy ...
- 4 PyExecJS模块
PyExecJS模块 pyexecjs是一个可以帮助我们运行js代码的一个第三方模块. 其使用是非常容易上手的. 但是它的运行是要依赖能运行js的第三方环境的. 这里我们选择用node作为我们运行js ...
- #珂朵莉树#CF896C Willem, Chtholly and Seniorious
题目 支持区间加,区间推平,询问区间第\(k\)小, 以及询问区间\(\sum{a_i^x}\pmod y\),数据随机 分析 由于数据随机,那么区间推平的概率为\(\frac{1}{4}\), 考虑 ...
- Java轻松实现,每天给对象发情话!
一.引言 最近看到一篇用js代码实现表白的文章,深有感触.然后发现自己也可以用java代码实现,然后就开始写代码了,发现还挺有意思的,话不多说开搞实现思路: 使用HttpClient远程获取彩虹屁生成 ...
- 知识图谱增强的KG-RAG框架
昨天我们聊到KG在RAG中如何发挥作用,今天我们来看一个具体的例子. 我们找到一篇论文: https://arxiv.org/abs/2311.17330 ,论文的研究人员开发了一种名为知识图谱增强的 ...
- .NET Emit 入门教程:第六部分:IL 指令:7:详解 ILGenerator 指令方法:分支条件指令
前言: 经过前面几篇的学习,我们了解到指令的大概分类,如: 参数加载指令,该加载指令以 Ld 开头,将参数加载到栈中,以便于后续执行操作命令. 参数存储指令,其指令以 St 开头,将栈中的数据,存储到 ...
- linux下升级openssh参考[不建议采用此法安装]
本人不见一采用这种方法安装,只是当遇到问题时候有一定的参考意义,所以贴了上来.建议使用yum方式安装,详见在下另一篇博文: http://blog.csdn.net/lqzixi/article/de ...
- AI极速批量换脸!Roop-unleashed下载介绍,可直播
要说AI换脸领域,最开始火的项目就是Roop了,Roop-unleashed作为Roop的嫡系分支,不仅继承了前者的强大基因,更是在功能上实现了重大突破与升级 核心特性 1.可以进行高精度的图片.视频 ...