在.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系统集成

版本回溯功能

  • 基于日志恢复历史状态
  • 可视化变更对比
  • 操作回滚接口设计
posted @ 2025-11-24 19:54  NetCoreKevin  阅读(45)  评论(0)    收藏  举报