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