前言:

  近日在项目协同开发过程中出现了问题,数据出现了异常;其他人员怀疑项目数据丢失程序存在问题。于是通过排查程序提供的审计日志最终还原了当时操作及原因。

  可见审计日志在排查、定位问题是相当有用的,那么在.Net Core 如何来实现审计日志呢?

  接下来一步步来实现效果

一、审计日志定义及作用

 审计日志:  

维基百科: “审计跟踪(也叫审计日志)是与安全相关的按照时间顺序的记录,记录集或者记录源,它们提供了活动序列的文档证据,这些活动序列可以在任何时间影响一个特定的操作,步骤或其他”

 作用:

  1、快速定位问题耗时及性能情况

  2、记录调用时环境信息:如浏览器、参数等

二、.Net Core 中实现审计日志

 那么怎么实现审计日志呢?其实核心思想很简单。包含以下步骤:

  • 获取调用接口方法时相关信息
  • 记录当前接口耗时情况
  • 保存审计日志信息到数据库中

  那么如何获取调用接口时相关信息呢?.Net Core中可以使用:过滤器、拦截器 实现。 

  本次示例中将采用过滤器实现审计日志实现功能;主要流程如下

java 电力日志审计设计_System

 

定义审计日志信息:  

public class AuditInfo
{
    /// <summary>
    /// 调用参数
    /// </summary>
    public string Parameters { get; set; }
    /// <summary>
    /// 浏览器信息
    /// </summary>
    public string BrowserInfo { get; set; }
    /// <summary>
    /// 客户端信息
    /// </summary>
    public string ClientName { get; set; }
    /// <summary>
    /// 客户端IP地址
    /// </summary>
    public string ClientIpAddress { get; set; }
    /// <summary>
    /// 执行耗时
    /// </summary>
    public int ExecutionDuration { get; set; }
    /// <summary>
    /// 执行时间
    /// </summary>
    public DateTime ExecutionTime { get; set; }
    /// <summary>
    /// 返回内容
    /// </summary>
    public string ReturnValue { get; set; }
    /// <summary>
    /// 异常对象
    /// </summary>
    public Exception Exception { get; set; }
    /// <summary>
    /// 方法名
    /// </summary>
    public string MethodName { get; set; }
    /// <summary>
    /// 服务名
    /// </summary>
    public string ServiceName { get; set; }
    /// <summary>
    /// 调用者信息
    /// </summary>
    public string UserInfo { get; set; }
    /// <summary>
    /// 自定义数据
    /// </summary>
    public string CustomData { get; set; }
}
  • 实现审计日志过滤器
using AuditLogDemo.Models;
using AuditLogDemo.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;

namespace AuditLogDemo.Fliters
{
    public class AuditLogActionFilter : IAsyncActionFilter
    {
        /// <summary>
        /// 审计日志服务对象
        /// </summary>
        private readonly IAuditLogService _auditLogService;
        /// <summary>
        /// 登录用户
        /// </summary>
        private readonly ISession _Session;
        /// <summary>
        /// 日志记录
        /// </summary>
        private readonly ILogger<AuditLogActionFilter> _logger;

        public AuditLogActionFilter(
            IAuditLogService auditLogService,
            ISession Session,
            ILogger<AuditLogActionFilter> logger
            )
        {
            _Session = Session;
            _logger = logger;
            _auditLogService = auditLogService;
        }

        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            // 判断是否写日志
            if (!ShouldSaveAudit(context))
            {
                await next();
                return;
            }
            //接口Type
            var type = (context.ActionDescriptor as ControllerActionDescriptor).ControllerTypeInfo.AsType();
            //方法信息
            var method = (context.ActionDescriptor as ControllerActionDescriptor).MethodInfo;
            //方法参数
            var arguments = context.ActionArguments;
            //开始计时
            var stopwatch = Stopwatch.StartNew();
            var auditInfo = new AuditInfo
            {
                UserInfo = _Session?.Id,
                ServiceName = type != null ? type.FullName.TruncateWithPostfix(EntityDefault.FieldsLength250) : "",
                MethodName = method.Name.TruncateWithPostfix(EntityDefault.FieldsLength250),
                ////请求参数转Json
                Parameters = JsonConvert.SerializeObject(arguments),
                ExecutionTime = DateTime.Now,
                BrowserInfo = context.HttpContext.Request.Headers["User-Agent"].ToString().TruncateWithPostfix(EntityDefault.FieldsLength250),
                ClientIpAddress = context.HttpContext.Connection.RemoteIpAddress.ToString().TruncateWithPostfix(EntityDefault.FieldsLength50),
                //ClientName = _clientInfoProvider.ComputerName.TruncateWithPostfix(EntityDefault.FieldsLength100),
                Id = Guid.NewGuid().ToString()
            };

            ActionExecutedContext result = null;
            try
            {
                result = await next();
                if (result.Exception != null && !result.ExceptionHandled)
                {
                    auditInfo.Exception = result.Exception;
                }
            }
            catch (Exception ex)
            {
                auditInfo.Exception = ex;
                throw;
            }
            finally
            {
                stopwatch.Stop();
                auditInfo.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds);

                if (result != null)
                {
                    switch (result.Result)
                    {
                        case ObjectResult objectResult:
                            auditInfo.ReturnValue = JsonConvert.SerializeObject(objectResult.Value);
                            break;

                        case JsonResult jsonResult:
                            auditInfo.ReturnValue = JsonConvert.SerializeObject(jsonResult.Value);
                            break;

                        case ContentResult contentResult:
                            auditInfo.ReturnValue = contentResult.Content;
                            break;
                    }
                }
                Console.WriteLine(auditInfo.ToString());
                //保存审计日志
                await _auditLogService.SaveAsync(auditInfo);
            }
        }

        /// <summary>
        /// 是否需要记录审计
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        private bool ShouldSaveAudit(ActionExecutingContext context)
        {
            if (!(context.ActionDescriptor is ControllerActionDescriptor))
                return false;
            var methodInfo = (context.ActionDescriptor as ControllerActionDescriptor).MethodInfo;

            if (methodInfo == null)
            {
                return false;
            }

            if (!methodInfo.IsPublic)
            {
                return false;
            }

            if (methodInfo.GetCustomAttribute<AuditedAttribute>() != null)
            {
                return true;
            }

            if (methodInfo.GetCustomAttribute<DisableAuditingAttribute>() != null)
            {
                return false;
            }

            var classType = methodInfo.DeclaringType;
            if (classType != null)
            {
                if (classType.GetTypeInfo().GetCustomAttribute<AuditedAttribute>() != null)
                {
                    return true;
                }

                if (classType.GetTypeInfo().GetCustomAttribute<AuditedAttribute>() != null)
                {
                    return false;
                }
            }
            return false;
        }
    }
}

   该内容为实现审计日志功能主要逻辑,通过过滤器获取当前执行控制器、方法判断是否需要记录审计日志;其他请求参数、客户端ip等相关基本信息组成审计日志对象,并记录调用时间。 

  • 注册过滤器
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(options =>
    {
        options.Filters.Add(typeof(AuditLogActionFilter));
    });

    //审计日志存储
    services.AddDbContext<AuditLogDBContent>(options =>
    {
        string conn = Configuration.GetConnectionString("LogDB");
        options.UseSqlite(conn);
    });
}

  到此审计日志主要逻辑已经实现完成。是不是很简单

三、总结

 回过头来看,在.net core 中需要统一监控或过滤时,可以采用过滤器(Filter)或拦截器来实现相关效果 

 .Net Core中 Filter 常件的有:Authorization Filter(认证过滤器),Resource Filter(资源过滤器),Exception Filter(异常过滤器),Action Filter(方法过滤器),Result Filter(结果过滤器)