在.Net项目的EFCore中如何实现敏感数据或者关键数据的变动日志记录关键字段(如密码、金额、权限等)的变更历史-确保日志可追溯且不可篡改

具体实现可参考NetCoreKevin中的Kevin.EntityFrameworkCore下的SaveChangesWithSaveLog方法

一个基于NET8搭建DDD-微服务-现代化Saas企业级WebAPI前后端分离架构:前端Vue3、IDS4单点登录、多级缓存、自动任务、分布式、AI智能体、一库多租户、日志、授权和鉴权、CAP事件、SignalR、领域事件、MCP协议服务、IOC模块化注入、Cors、Quartz自动任务、多短信、AI、AgentFramework、SemanticKernel集成、RAG检索增强+Qdrant矢量数据库、OCR识别、API多版本、单元测试、RabbitMQ

项目地址:github:https://github.com/junkai-li/NetCoreKevin
Gitee: https://gitee.com/netkevin-li/NetCoreKevin

技术文章大纲:在.NET项目的EFCore中实现敏感数据变动日志

核心目标

  • 记录关键字段(如密码、金额、权限等)的变更历史
  • 确保日志可追溯且不可篡改
  • 平衡性能与安全性需求

技术实现方案

拦截数据变更

  • 重写SaveChangesSaveChangesAsync方法
  • 利用ChangeTracker获取变更的实体状态
  • 筛选标记为敏感字段的属性变更

自定义日志模型设计

    public partial class TOSLog : CD     {         /// <summary>         /// 外链表名         /// </summary>         [StringLength(50)]         [Description("外链表名")]         public string? Table { get; set; }            /// <summary>         /// 外链表ID         /// </summary>         [Description("外链表ID")]         public Guid TableId { get; set; }            /// <summary>         /// 标记         /// </summary>         [StringLength(100)]         [Description("标记")]         public string? Sign { get; set; }            /// <summary>         /// 变动内容         /// </summary>          [Description("变动内容")]         public string? Content { get; set; }            /// <summary>         /// 操作人信息         /// </summary>         [Description("操作人信息")]         public Guid? ActionUserId { get; set; }         public virtual TUser? ActionUser { get; set; }            /// <summary>         /// 备注         /// </summary>          [Description("备注")]         public string? Remarks { get; set; }            /// <summary>         /// Ip地址         /// </summary>         [StringLength(100)]         [Description("Ip地址")]         public string? IpAddress { get; set; }            /// <summary>         ///  设备标记         /// </summary>          [Description("设备标记")]         public string? DeviceMark { get; set; }     } 

敏感数据脱敏处理

  • 对密码等字段采用哈希存储
  • 金额类字段保留变更差值
  • 使用[SensitiveData]自定义属性标记敏感字段

*核心代码数据变化比较

        ///// <summary>        ///// 数据变化比较        ///// </summary>        ///// <typeparam name="T"></typeparam>        ///// <param name="original"></param>        ///// <param name="after"></param>        ///// <returns></returns>        public string ComparisonEntity<T>(T original, T after) where T : new()        {            var retValue = "";             var fields = typeof(T).GetProperties();             var baseTypeNames = new List<string>();            var baseType = original.GetType().BaseType;            while (baseType != null)            {                baseTypeNames.Add(baseType.FullName);                baseType = baseType.BaseType;            }             for (int i = 0; i < fields.Length; i++)            {                var pi = fields[i];                 string oldValue = pi.GetValue(original)?.ToString();                string newValue = pi.GetValue(after)?.ToString();                 string typename = pi.PropertyType.FullName;                 if ((typename != "System.Decimal" && oldValue != newValue) || (typename == "System.Decimal" && decimal.Parse(oldValue) != decimal.Parse(newValue)))                {                    var descriptionAttr = pi.GetCustomAttributes(typeof(DescriptionAttribute), true);                    if (descriptionAttr.Length > 0)                    {                        retValue += ((DescriptionAttribute)descriptionAttr[0]).Description + ":";                    }                    else                    {                        retValue += pi.Name + ":";                    }                     if (pi.Name != "Id" & pi.Name.EndsWith("Id"))                    {                        var foreignTable = fields.FirstOrDefault(t => t.Name == pi.Name.Replace("Id", ""));                         using var db = new KevinDbContext();                         var foreignName = foreignTable.PropertyType.GetProperties().Where(t => t.CustomAttributes.Where(c => c.AttributeType.Name == "ForeignNameAttribute").Count() > 0).FirstOrDefault();                         if (foreignName != null)                        {                             if (oldValue != null)                            {                                var oldForeignInfo = db.Find(foreignTable.PropertyType, Guid.Parse(oldValue));                                oldValue = foreignName.GetValue(oldForeignInfo).ToString();                            }                             if (newValue != null)                            {                                var newForeignInfo = db.Find(foreignTable.PropertyType, Guid.Parse(newValue));                                newValue = foreignName.GetValue(newForeignInfo).ToString();                            }                         }                         retValue += (oldValue ?? "") + " -> ";                        retValue += (newValue ?? "") + "; n";                     }                    else if (typename == "System.Boolean")                    {                        retValue += (oldValue != null ? (bool.Parse(oldValue) ? "是" : "否") : "") + " -> ";                        retValue += (newValue != null ? (bool.Parse(newValue) ? "是" : "否") : "") + "; n";                    }                    else if (typename == "System.DateTime")                    {                        retValue += (oldValue != null ? DateTime.Parse(oldValue).ToString("yyyy-MM-dd") : "") + " ->";                        retValue += (newValue != null ? DateTime.Parse(newValue).ToString("yyyy-MM-dd") : "") + "; n";                    }                    else                    {                        retValue += (oldValue ?? "") + " -> ";                        retValue += (newValue ?? "") + "; n";                    }                 }               }             return retValue;        } 

核心代码重写SaveChanges

 KevinDbContext db = this;   var list = db.ChangeTracker.Entries().Where(t => t.State == EntityState.Modified).ToList();  foreach (var item in list)  {      #region 更改值时处理乐观并发      item.Entity.GetType().GetProperty("RowVersion")?.SetValue(item.Entity, Guid.NewGuid());      #endregion       var type = item.Entity.GetType();       var oldEntity = item.OriginalValues.ToObject();       var newEntity = item.CurrentValues.ToObject();       var entityId = item.CurrentValues.GetValue<Guid>("Id");         object[] parameters = { oldEntity, newEntity };       var result = new KevinDbContext().GetType().GetMethod("ComparisonEntity").MakeGenericMethod(type).Invoke(new KevinDbContext(), parameters);       var osLog = new TOSLog();      osLog.Id = Guid.NewGuid();      osLog.CreateTime = DateTime.Now;      osLog.Table = type.Name;      osLog.TableId = entityId;      osLog.Sign = "Modified";      osLog.Content = result.ToString();      osLog.IpAddress = HttpContextAccessor.GetIpAddress();      osLog.DeviceMark = HttpContextAccessor.GetDevice();      osLog.ActionUserId = CurrentUser.UserId;      osLog.TenantId = TenantId;      db.Set<TOSLog>().Add(osLog);  }   #region 新增处理多租户   var Addedlist = db.ChangeTracker.Entries().Where(t => t.State == EntityState.Added).ToList();  foreach (var item in Addedlist)  {      item.Entity.GetType().GetProperty("TenantId")?.SetValue(item.Entity, TenantId);  }   #endregion  return base.SaveChanges(); 

高级优化策略

异步日志写入

  • 通过Task.Run实现非阻塞写入
  • 考虑使用内存队列缓冲日志
  • 设置合理的重试机制

性能监控

  • 记录日志写入耗时
  • 配置日志表索引优化查询
  • 定期归档历史日志

安全增强措施

日志加密存储

  • 对敏感字段采用AES加密
  • 实现日志签名防篡改
  • 设置最小化访问权限

合规性检查

  • GDPR等法规要求处理
  • 设置合理的日志保留周期
  • 提供日志清理接口

扩展应用场景

实时告警机制

  • 关键字段变更触发通知
  • 可疑操作模式检测
  • 与SIEM系统集成

版本回溯功能

  • 基于日志恢复历史状态
  • 可视化变更对比
  • 操作回滚接口设计
发表评论

评论已关闭。

相关文章