当前位置: 代码网 > it编程>编程语言>Asp.net > ASP.NET Core实现动态审计日志功能

ASP.NET Core实现动态审计日志功能

2024年05月15日 Asp.net 我要评论
前言最近一直在写 go 和 python ,好久没写 c# ,重新回来写 c# 代码时竟有一种亲切感~说回正题。在当今这个数字化迅速发展的时代,每一个操作都可能对业务产生深远的影响,无论是对数据的简单

前言

最近一直在写 go 和 python ,好久没写 c# ,重新回来写 c# 代码时竟有一种亲切感~

说回正题。

在当今这个数字化迅速发展的时代,每一个操作都可能对业务产生深远的影响,无论是对数据的简单查询,还是对系统配置的修改。在这样的背景下,审计日志不仅仅是一种遵循最佳实践的手段,更是确保数据安全、提高系统透明度、促进责任归属明晰的关键工具。通过详细记录谁在何时对系统进行了何种操作,审计日志帮助组织追踪用户活动,分析系统问题,甚至在发生安全事件时,提供必要的线索进行调查。

实现审计日志的方法多样,但如何在不干扰主业务逻辑的同时,高效地集成这一功能,是开发者们面临的一大挑战。本文着重探讨如何借鉴面向切面编程(aspect-oriented programming, aop)的设计思想,在asp.net core应用中以最小化代码侵入性实现动态审计日志功能。aop允许我们通过预定义的模式,如日志记录、性能统计和安全控制,以声明的方式增强代码功能,而无需修改实际的业务逻辑代码。

本文将指导读者从概念的理解到具体的实施,再到最终的数据持久化处理,特别是如何利用mongodb这一强大的nosql数据库来持久化审计日志数据。无论你是刚刚接触asp.net core的新手,还是寻求为现有项目增加审计功能的资深开发者,本文都将提供从理论到实践的全面指导。通过本文,你将学习到如何设计和实现一个灵活、可扩展的审计日志系统,同时保持对主业务逻辑的最小化干扰。

让我们开始这一旅程,一步步探索如何在asp.net core应用中集成高效、灵活的审计日志机制,利用aop设计思想实现高度解耦和动态增强的系统功能。

审计日志基础

定义和用途

审计日志有助于追踪用户的操作行为、数据变更记录以及系统的安全性分析等。

常用的审计日志有这些类型。

  • 操作审计:记录用户对系统的所有操作,例如登录、登出、数据增删改查等。
  • 数据审计:记录数据的变更详情,如记录数据修改前后的值。
  • 安全审计:记录安全相关事件,如失败的登录尝试、权限变更等。
  • 性能审计:记录关键操作的性能数据,帮助分析系统瓶颈。

本文的代码以实现操作审计为例。

模型定义&关键信息

审计日志是系统安全和管理的关键部分,它帮助我们理解系统内发生了什么、何时发生、由谁触发。为了实现这一目标,审计日志记录需要包含几个关键的组成部分。

  • eventid 是每条审计记录的唯一标识符。就像每个人都有一个独一无二的身份证号一样,每条审计日志也有一个独特的eventid。这使我们能够轻松地找到和引用特定的审计事件。
  • eventtype 描述了发生的事件类型。这告诉我们这条记录是关于什么的——是用户登录、数据修改,还是权限更改等。通过查看eventtype,我们可以快速了解记录的核心信息,而无需深入研究细节。
  • userid 是触发事件的用户的标识。在审计日志中记录userid非常重要,因为它帮助我们追踪谁负责了什么操作。如果发现了问题或者不当行为,我们可以通过userid来确定责任人。

设计审计日志模型

auditlog 类

新建 auditlog.cs 类,每个字段都有注释,我就不再赘述了。

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>
  /// 被操作的实体名称
  /// </summary>
  public string entityname { get; set; }

  /// <summary>
  /// 被操作的实体标识
  /// </summary>
  public string entityid { get; set; }

  /// <summary>
  /// 修改前的数据,可根据实际情况以json格式存储
  /// </summary>
  public string? originalvalues { get; set; }

  /// <summary>
  /// 修改后的数据,可根据实际情况以json格式存储
  /// </summary>
  public string? currentvalues { get; set; }

  /// <summary>
  /// 具体的更改内容,可根据实际情况以json格式存储
  /// </summary>
  public string? changes { get; set; }

  /// <summary>
  /// 事件描述
  /// </summary>
  public string? description { get; set; }
}

捕获审计日志

iauditlogservice 接口

先写一个接口,用来操作审计日志。使用接口可以保持代码的整洁和重用,同时也便于将来对审计日志记录逻辑进行扩展或修改。

为了简单起见,目前这里我们只写了一个记录的方法。

public interface iauditlogservice {
  task logasync(auditlog auditlog);
}

之后在依赖注入容器里注册(假设实现类的名称为 auditlogservice

builder.services.addscope<iauditlogservice, auditlogservice>();

这个设计既保持了代码的清晰与简洁,也为将来可能的需求变更(如改变审计日志的存储方式、增加审计字段等)提供了足够的灵活性。

具体实现会在后续的数据持久化部分介绍。

actionfilter 方式

在asp.net core中,action过滤器提供了一种强大的机制,允许我们在控制器的动作执行前后插入自定义逻辑。

我们可以在不修改现有业务逻辑代码的情况下,自动地捕获用户的操作以及数据的更改。这种方式充分利用了aop的思想,实现了代码的最小化侵入。

创建 auditlogattribute 类

直接上代码了,继承自 actionfilterattribute 类,可以实现一个 action 过滤器的特性,其中 eventtype 和 entityname 我设计成需要手动指定,其他的属性可以通过各种方法来获取。

public class auditlogattribute : actionfilterattribute {
  public string eventtype { get; set; }
  public string entityname { 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),
        entityname = this.entityname,
        entityid = ctxitems["auditlog_entityid"]?.tostring() ?? "",
        originalvalues = ctxitems["auditlog_originalvalues"]?.tostring(),
        currentvalues = ctxitems["auditlog_currentvalues"]?.tostring(),
        changes = ctxitems["auditlog_changes"]?.tostring(),
        description = $"操作类型:{this.eventtype},实体名称:{this.entityname}",
      };

      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.");
    }
  }
}

注意事项

  • 异常处理:考虑到日志记录不应影响主要业务流程的执行,需要添加异常处理逻辑,确保即使日志记录过程中发生异常,也不会干扰到正常的业务逻辑。
  • 性能问题:虽然已经在异步方法中记录审计日志,但如果审计日志的记录过程很慢,可能会略微延迟响应时间。可以使用批处理、缓存来异步写入数据库,或者将记录逻辑放到后台任务、消息队列中。

获取ip地址

通过httpcontext.connection.remoteipaddress属性可以获取 ip 地址,但如果应用部署在了代理服务器后面(例如使用了负载均衡器),直接获取的ip地址可能是代理服务器的地址,而不是客户端的真实ip地址。

所以这里我封装了 getipaddress 方法

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();
}

首先尝试从x-forwarded-for请求头中获取ip地址,这是一个标准的http头,用于识别通过http代理或负载均衡器发送请求的客户端的原始ip地址。如果请求没有经过代理,或者想要获取代理服务器的地址,那么它会回退到使用httpcontext.connection.remoteipaddress

x-forwarded-for可能包含多个ip地址(如果请求通过多个代理传递),因此代码中使用了split(',')来处理这种情况,并且仅取第一个ip地址作为客户端的真实ip地址。

使用方法

经过封装后可以很方便的使用这个审计功能了,只需要在接口上添加一行代码就可以实现审计功能。

[auditlog(eventtype = nameof(setsubtaskfeedback), entityname = nameof(subtask))]
[httppost("sub-tasks/{subid}/set-feedback")]
public async task<apiresponse> setsubtaskfeedback(string subid, [frombody] subtaskfeedbackdto dto) {}

手动记录方式

尽管使用action过滤器是一种高效的自动化方式,但在某些情况下,需要更精细地控制审计日志的记录。这时候只能修改接口代码,在业务逻辑里加入审计日志记录。

这种方式虽然需要直接修改业务代码,但它提供了最大的灵活性和控制能力。

这个代码就没什么特别的了,直接在接口中调用 iauditlogservice 的 logasync 方法来记录审计日志即可。

通过 httpcontext 共享数据

有些参数是很难在 actionfilter 里自动获取到的,这些往往跟业务逻辑是有关的,这时候 httpcontext 就成为了一个理想的桥梁。

我们可以将一些临时数据,比如操作前的数据快照,存储在 httpcontext.items 中,然后在过滤器中访问这些数据来完成审计日志的记录。这种方法不仅保持了代码的解耦,还允许我们灵活地在应用的不同部分共享数据。

httpcontext.items是一个键值对集合,可用于在一个请求的生命周期内共享数据。

这样在接口中的代码就是

httpcontext.items["auditlog_originalvalues"] = item.feedbackid;
httpcontext.items["auditlog_currentvalues"] = dto.feedbackid;
httpcontext.items["auditlog_changes"] = $"更新反馈结果 {item.feedbackid} -> {dto.feedbackid}";

注意事项

  • 确保业务逻辑和auditlogattribute中使用的键(如 auditlog_originalvalues )唯一且一致,以避免潜在的冲突。这里最好是自己封装一个 class 来提供这些 const ;
  • 如果业务逻辑抽象到了 service 层,则需要注入 ihttpcontextaccessor 才能访问 httpcontext ,这个服务可以通过 services.addhttpcontextaccessor() 来注册;

日志持久化

审计日志的有效持久化是确保长期安全和合规性的关键。

选择存储方案

在选择最合适的存储方案时,需要考虑数据的重要性、查询的频率、成本以及维护的复杂性等多个因素。

关系型数据库(rds)

关系型数据库,如mysql、postgresql等,以其稳定性和成熟性受到广泛认可。它们提供了严格的数据完整性保障和复杂查询的强大能力,适合需要执行复杂分析和报告的审计日志。

  • 优点:数据结构化、支持复杂查询、成熟的管理工具。
  • 缺点:相对较高的成本、可能需要复杂的架构来支持大规模数据。

nosql数据库

nosql数据库,如mongodb、cassandra等,提供了灵活的数据模型和良好的横向扩展能力,适合于结构多变或数据量巨大的审计日志。

  • 优点:高可扩展性、灵活的数据模型、快速的写入速度。
  • 缺点:查询功能相对有限、数据一致性模型较弱。

文件系统

直接将审计日志写入文件系统是最直接的存储方式,适用于日志量不是特别大或对查询需求不高的场景。

  • 优点:实现简单、成本低廉、易于迁移;
  • 缺点:查询和分析不便、难以管理大量日志文件、扩展性有限。

每种存储方案都有其适用场景,因此选择哪一种方案应根据具体需求和资源情况综合考虑。对于需要快速写入和高度可扩展的审计日志系统,nosql数据库是一个不错的选择。

因此本文选择了 mongodb 来记录日志。

选择mongodb作为审计日志的存储方案,不仅因为它的高性能和可扩展性,还因为它支持灵活的文档数据模型,使得存储非结构化或半结构化的审计数据变得简单。

实现 auditlogmongoservice

在 c# 中使用 mongodb 非常简单。

需要先添加 mongodb.driver 的 nuget 包

dotnet add mongodb.driver

直接上代码吧,

public class auditlogmongoservice : iauditlogservice {
  private readonly imongocollection<auditlog> _auditlogs;

  public auditlogmongoservice(string connectionstring, string databasename) {
    var client = new mongoclient(connectionstring);
    var database = client.getdatabase(databasename);
    _auditlogs = database.getcollection<auditlog>("audit_logs");
  }

  public async task logasync(auditlog auditlog) {
    await _auditlogs.insertoneasync(auditlog);
  }
}

准备连接字符串&注册服务

为了避免硬编码,将连接字符串放在配置文件(appsettings.json)里

"connectionstrings": {
  "redis": "redis:6379",
  "mongodb": "mongodb://username:password@path-to-mongo:27017"
}

注册服务

builder.services.addsingleton<iauditlogservice>(sp => new auditlogmongoservice(builder.configuration.getconnectionstring("mongodb"), "db_name"));

搞定~

部署 mongodb

附上 mongodb 的部署方法吧,我这里使用 docker ,很方便

version: '3.1'

services:

  mongo:
    image: mongo:4.4.6
    restart: always
    volumes:
      - ./data:/data/db
    environment:
      mongo_initdb_root_username: username
      mongo_initdb_root_password: password
    ports:
      - 27017:27017

  mongo-express:
    image: mongo-express
    restart: always
    environment:
      me_config_mongodb_adminusername: username
      me_config_mongodb_adminpassword: password
      me_config_mongodb_url: mongodb://username:password@mongo:27017/
    ports:
      - 8081:8081

使用 docker-compose 来编排,映射了 27017 和 8081 端口

可以使用 8081 端口访问 mongo-express 网页服务

如何查看日志

  • 使用 mongodb compass 这个软件来查看数据
  • 使用 mongo-express 服务可以在网页上查看数据

小结

虽然是比较简单的功能,不过使用 aop 来实现用起来感觉还是蛮爽的,不得不说 aspnetcore 的功能确实丰富~

以上就是asp.net core实现动态审计日志功能的详细内容,更多关于asp.net core审计日志的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com