Abp(PasteTemplate)项目如何添加对审计日志Auditing的支持

PasteTemplate作为Abp vNext项目的精简版,也就是阉割版!

这个精简的原则是能不要的都不要,所以Auditing也很荣幸的被移除了!

如果有需求,需要加回去,咋办?

我的项目是Volo.Abp的8.2.0版本为例

(我的项目叫PasteSpider,下方的XXX在我项目中就是PasteSpider)

Volo.Abp.AuditLogging.Domain

在XXX.Domain中引入

      <PackageReference Include="Volo.Abp.AuditLogging.Domain" Version="8.2.0" />

对应的XXXDomainModule.cs中添加如下代码

    /// <summary>
    /// 
    /// </summary>
    [DependsOn(
        typeof(AbpAuditLoggingDomainModule),
        typeof(AbpDddDomainModule)
    )]

Volo.Abp.AuditLogging.EntityFrameworkCore

在XXX.EntityFrameworkCore中引入

	  <PackageReference Include="Volo.Abp.AuditLogging.EntityFrameworkCore" Version="8.2.0" />

同样的,在XXXEntityFrameworkCoreModule.cs中加入

    /// <summary>
    /// 
    /// </summary>
    [DependsOn(
                typeof(AbpAuditLoggingEntityFrameworkCoreModule),
                typeof(PasteSpiderDomainModule),
        typeof(AbpEntityFrameworkCoreModule)
    )]

AuditingDbContext

添加一个AuditingDbContext.cs,作为数据库上下文用

    /// <summary>
    /// 
    /// </summary>
    [ConnectionStringName(PasteSpiderDbProperties.AbpAuditLogging)]
    public class AuditingDbContext : AbpAuditLoggingDbContext
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="options"></param>
        public AuditingDbContext(DbContextOptions<AbpAuditLoggingDbContext> options) : base(options)
        {
        }

        /// <summary>
        /// 
        /// </summary>
        public DbSet<AuditLogAction> AuditLogAction { get; set; }

        /// <summary>
        /// 
        /// </summary>
        public DbSet<EntityChange> AuditLogEntityChange { get; set; }

        /// <summary>
        /// 
        /// </summary>
        public DbSet<EntityPropertyChange> AuditLogEntityPropertyChange { get; set; }
    }

AuditingDbContextFactory

如果使用add-migration的话,你还需要一个Factory

创建AuditingDbContextFactory.cs

using System;
using System.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
using Volo.Abp.AuditLogging.EntityFrameworkCore;

namespace PasteSpider
{
    /// <summary>
    /// 
    /// </summary>
    public class AuditingDbContextFactory : IDesignTimeDbContextFactory<AuditingDbContext>
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="args"></param>
        /// <returns></returns>
        public AuditingDbContext CreateDbContext(string[] args)
        {
            //这个仅仅在EF的时候调用
            //System.Console.WriteLine($"{System.DateTime.Now} AuditingDbContextFactory.CreateDbContext");
            //AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
            var configuration = BuildConfiguration();
            var builder = new DbContextOptionsBuilder<AbpAuditLoggingDbContext>()
                .UseNpgsql(configuration.GetConnectionString(PasteSpiderDbProperties.AbpAuditLogging));
            builder.EnableSensitiveDataLogging();

            return new AuditingDbContext(builder.Options);
        }

        private static IConfigurationRoot BuildConfiguration()
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: false);

            return builder.Build();
        }
    }
}

然后修改PasteSpiderDbProperties.cs,如下

        /// <summary>
        /// 审计日志的连接字符串
        /// </summary>
        public const string AbpAuditLogging = "AbpAuditLogging";

这个值可不是随意的哈,我从源码看到他会读取appsettings.json中的ConnectionStrings:AbpAuditLogging作为数据库的链接字符串!

然后在PasteSpider.HttpApi.Host的PasteSpiderHttpApiHostModule.cs中的ConfigureServices

                context.Services.Configure<AbpAuditingOptions>(options =>
                {
                    //可以使用过滤器限定,是否需要记录日志!
                    options.IsEnabled = true;
                    options.IsEnabledForGetRequests = false;//get的都忽略
                    
                    options.Contributors.Add(new MyAuditLogContributor());//给日志添加用户信息等

                });

MyAuditLogContributor

具体如何配置看你自己的实际需求,这里还有一个文件MyAuditLogContributor,我的理解就是修改审计日志的信息用的

using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Auditing;
using Volo.Abp.Clients;

namespace PasteSpider
{
    /// <summary>
    /// 
    /// </summary>
    public class MyAuditLogContributor : AuditLogContributor
    {

        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        public override void PreContribute(AuditLogContributionContext context)
        {
            //var currentUser = context.ServiceProvider.GetRequiredService<ICurrentUser>();
            //context.AuditInfo.ClientId = currentUser.UserName;
            //Console.WriteLine($"PreContribute --- {currentUser?.UserName}");
            //context.AuditInfo.SetProperty(
            //    "ClientId",
            //    currentUser.FindClaimValue(AbpClaimTypes.ClientId)
            //);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        public override void PostContribute(AuditLogContributionContext context)
        {
            //在这里给对象赋值
            var currentUser = context.ServiceProvider.GetRequiredService<ICurrentClient>();
            //Console.WriteLine($"PostContribute --- {currentUser?.Id}");
            context.AuditInfo.ClientId = currentUser.Id.AutoSubString(32);//这样泄漏的就不是完整的密钥了
                                                                       //context.AuditInfo.Comments.Add("Some comment...");
                                                                       //1719879380_1_admin_028fabe76af2cf8ec5eccc7571900381
        }
    }
}

上面我的作用就是,往ICurrentClient写入对应的密钥,用于记录谁操作的,由于我没有使用官方的授权那一块,所以我需要在对应的授权过滤器中写入这个clientid!

在RoleAttribute中获取到token后写入到Principal中,比如:

    /// <summary>
    /// 管理账号的权限筛选
    /// </summary>
    public class RoleAttribute : ActionFilterAttribute
    {
        /// <summary>
        /// 权限
        /// </summary>
        private string _role { set; get; }

        /// <summary>
        /// 角色
        /// </summary>
        private string _model { get; set; }

        /// <summary>
        /// 
        /// </summary>
        public bool IsRoot { get { if (!String.IsNullOrEmpty(_model)) { return _model == "root"; } return false; } }

        /// <summary>
        /// 
        /// </summary>
        private readonly SmartHelper _appCache;

        /// <summary>
        /// 
        /// </summary>
        private ICurrentPrincipalAccessor _currentPrincipalAccessor;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="appcache"></param>
        /// <param name="currentPrincipalAccessor"></param>
        /// <param name="Model"></param>
        /// <param name="Role"></param>
        public RoleAttribute(
            SmartHelper appcache,
            ICurrentPrincipalAccessor currentPrincipalAccessor,
            string Model = default,
            string Role = default
            )
        {
            _role = Role;
            _model = Model;
            _appCache = appcache;
            _currentPrincipalAccessor = currentPrincipalAccessor;
            if (String.IsNullOrEmpty(_role))
            {
                _role = "view";
            }
            if (String.IsNullOrEmpty(_model))
            {
                _model = "data";
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        /// <exception cref="SmartShopException"></exception>
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            var needauth = true;
            foreach (var item in context.Filters)
            {
                if (item is RoleAttribute)
                {
                    needauth = (item == this);
                }
            }

            if (needauth)
            {
                //authorization
                if (context.HttpContext.Request.Headers[PublicString.AdminTokenHeadName].Count == 0)
                {
                    throw new SmartShopException("当前未登录,请登录后重试", "401");
                }
                var token = context.HttpContext.Request.Headers[PublicString.AdminTokenHeadName].ToString();
                if (token.StartsWith("Bearer"))
                {
                    token = token.Replace("Bearer ", "");
                }

                //这里可以做token的安全校验
                if (!String.IsNullOrEmpty(token) && token != "null" && token != "undefined")
                {

//-------------------------关键代码在这里-----------------------------------
                    var list = new System.Security.Claims.ClaimsIdentity();
                    list.AddClaim(new System.Security.Claims.Claim(AbpClaimTypes.ClientId, token));
                    _currentPrincipalAccessor.Principal.AddIdentity(list);
//=========================================================================
                    var back = _appCache.HasRole(_model, _role, token);
                    if (!back.role)
                    {
                        throw new SmartShopException($"{(back.code == 401 ? "当前登录密钥失效,请重新登录" : $"没有当前接口的操作权限,请确认! 需要权限{_model}-{_role}")}", $"{back.code}");
                    }
                }
                else
                {
                    throw new SmartShopException("当前密钥信息有误,请登录后重试 Empty !", "401");
                }
            }
            base.OnActionExecuting(context);
        }
    }

然后跑到总入口的OnApplicationInitialization中

                app.UseAuditing();

完成以上操作后,就可以重新生成项目了,然后

add-migration auditing_init_db -Context AuditingDbContext

然后就是发布 如何update-database看你自己的需求哈!

关于AuditingDbContext里面用哪种数据库,看你自己的需求,记住要去配置中更改对应的数据库连接串!

AbpAuditingMiddleware

以上操作完成后,你会发觉审计日志的[DisableAuditing]和[Audited]无法生效!!!

查看app.UseAuditing();的代码转到中间件AbpAuditingMiddleware,看到如下的一个函数

   /// <summary>
   /// 
   /// </summary>
   /// <param name="auditLogInfo"></param>
   /// <param name="httpContext"></param>
   /// <param name="hasError"></param>
   /// <returns></returns>
   private async Task<bool> ShouldWriteAuditLogAsync(AuditLogInfo auditLogInfo, HttpContext httpContext, bool hasError)
   {

       foreach (Func<AuditLogInfo, Task<bool>> alwaysLogSelector in AuditingOptions.AlwaysLogSelectors)
       {
           if (await alwaysLogSelector(auditLogInfo).ConfigureAwait(continueOnCapturedContext: false))
           {
               return true;
           }
       }

       if (AuditingOptions.AlwaysLogOnException && hasError)
       {
           return true;
       }

       if (!AuditingOptions.IsEnabledForAnonymousUsers && !CurrentUser.IsAuthenticated)
       {
           return false;
       }

       if (!AuditingOptions.IsEnabledForGetRequests && (string.Equals(httpContext.Request.Method, HttpMethods.Get, StringComparison.OrdinalIgnoreCase) || string.Equals(httpContext.Request.Method, HttpMethods.Head, StringComparison.OrdinalIgnoreCase)))
       {
           return false;
       }

       return true;
   }

仔细看的话,我感觉这个函数的逻辑是有问题的,函数的返回值最后兜底的是true,那么上面的应该就是只要判断false的才对,如上图,如果第一个逻辑块触发了true,那么后面的配置都将无效了!!!

这里没有看到关于过滤器的,其实在其他地方使用了过滤器的判断,但是作为我的需求来说,不合适,所以把整个文件修改下,如下:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Auditing;
using Volo.Abp.AspNetCore.Middleware;
using Volo.Abp.Auditing;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Uow;
using Volo.Abp.Users;

namespace PasteSpider
{
    /// <summary>
    /// 
    /// </summary>
    public class MyAuditingMiddleware : AbpMiddlewareBase, ITransientDependency
    {
        /// <summary>
        /// 
        /// </summary>
        private readonly IAuditingManager _auditingManager;

        /// <summary>
        /// 
        /// </summary>
        protected AbpAuditingOptions AuditingOptions { get; }

        /// <summary>
        /// 
        /// </summary>
        protected AbpAspNetCoreAuditingOptions AspNetCoreAuditingOptions { get; }

        /// <summary>
        /// 
        /// </summary>
        protected ICurrentUser CurrentUser { get; }

        /// <summary>
        /// 
        /// </summary>
        protected IUnitOfWorkManager UnitOfWorkManager { get; }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="auditingManager"></param>
        /// <param name="currentUser"></param>
        /// <param name="auditingOptions"></param>
        /// <param name="aspNetCoreAuditingOptions"></param>
        /// <param name="unitOfWorkManager"></param>
        public MyAuditingMiddleware(IAuditingManager auditingManager, ICurrentUser currentUser, IOptions<AbpAuditingOptions> auditingOptions, IOptions<AbpAspNetCoreAuditingOptions> aspNetCoreAuditingOptions, IUnitOfWorkManager unitOfWorkManager)
        {
            _auditingManager = auditingManager;
            CurrentUser = currentUser;
            UnitOfWorkManager = unitOfWorkManager;
            AuditingOptions = auditingOptions.Value;
            AspNetCoreAuditingOptions = aspNetCoreAuditingOptions.Value;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        /// <param name="next"></param>
        /// <returns></returns>
        public override async Task InvokeAsync(HttpContext context, RequestDelegate next)
        {
            if (await ShouldSkipAsync(context, next).ConfigureAwait(continueOnCapturedContext: false) || !AuditingOptions.IsEnabled || IsIgnoredUrl(context))
            {
                await next(context).ConfigureAwait(continueOnCapturedContext: false);
                return;
            }

            bool hasError = false;
            using IAuditLogSaveHandle saveHandle = _auditingManager.BeginScope();
            try
            {
                _ = 2;
                try
                {
                    await next(context).ConfigureAwait(continueOnCapturedContext: false);
                    if (_auditingManager.Current.Log.Exceptions.Any())
                    {
                        hasError = true;
                    }
                }
                catch (Exception item)
                {
                    hasError = true;
                    if (!_auditingManager.Current.Log.Exceptions.Contains(item))
                    {
                        _auditingManager.Current.Log.Exceptions.Add(item);
                    }

                    throw;
                }
            }
            finally
            {
                if (await ShouldWriteAuditLogAsync(_auditingManager.Current.Log, context, hasError).ConfigureAwait(continueOnCapturedContext: false))
                {
                    if (UnitOfWorkManager.Current != null)
                    {
                        try
                        {
                            await UnitOfWorkManager.Current.SaveChangesAsync().ConfigureAwait(continueOnCapturedContext: false);
                        }
                        catch (Exception item2)
                        {
                            if (!_auditingManager.Current.Log.Exceptions.Contains(item2))
                            {
                                _auditingManager.Current.Log.Exceptions.Add(item2);
                            }
                        }
                    }

                    await saveHandle.SaveAsync().ConfigureAwait(continueOnCapturedContext: false);
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        private bool IsIgnoredUrl(HttpContext context)
        {
            HttpContext context2 = context;
            if (context2.Request.Path.Value == null)
            {
                return false;
            }

            if (!AuditingOptions.IsEnabledForIntegrationServices && context2.Request.Path.Value.StartsWith("/integration-api/", StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }

            if (AspNetCoreAuditingOptions.IgnoredUrls.Any((string x) => context2.Request.Path.Value.StartsWith(x, StringComparison.OrdinalIgnoreCase)))
            {
                return true;
            }

            return false;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="auditLogInfo"></param>
        /// <param name="httpContext"></param>
        /// <param name="hasError"></param>
        /// <returns></returns>
        private async Task<bool> ShouldWriteAuditLogAsync(AuditLogInfo auditLogInfo, HttpContext httpContext, bool hasError)
        {
            if (AuditingOptions.AlwaysLogOnException && hasError)
            {
                return true;
            }

            if (!AuditingOptions.IsEnabledForAnonymousUsers && !CurrentUser.IsAuthenticated)
            {
                return false;
            }

            if (!AuditingOptions.IsEnabledForGetRequests && string.Equals(httpContext.Request.Method, HttpMethods.Get, StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            var _end = httpContext.GetEndpoint();
            if (_end != null)
            {
                if (_end.Metadata != null)
                {
                    var _size = _end.Metadata.Count;
                    if (_size > 0)
                    {
                        for (var k = _size - 1; k >= 0; k--)
                        {
                            var _item = _end.Metadata[k];
                            if (_item.GetType() == typeof(AuditedAttribute))
                            {
                                break;//跳出循环,由后续的判断接手
                            }
                            if (_item.GetType() == typeof(DisableAuditingAttribute))
                            {
                                return false;
                            }
                        }
                    }
                }
            }

            //维护下异步?
            if (hasError)
            {
                await Task.Delay(1);
            }

            //以下代码感觉鸡肋了
            //foreach (Func<AuditLogInfo, Task<bool>> alwaysLogSelector in AuditingOptions.AlwaysLogSelectors)
            //{
            //    if (await alwaysLogSelector(auditLogInfo).ConfigureAwait(continueOnCapturedContext: false))
            //    {
            //        return true;
            //    }
            //}

            return true;
        }
    }
}

先按照上图的进行修改,后续再考虑是否要基于完整的逻辑修改,也就是是否需要中途返回true的情况,还有就是Attribute在Class和Method的作用域的问题!理论上我们当前希望Method的作用权重大于Class的,通过观察,Metadata的排序是倒叙的,也就是Method的游标更大!

然后上面的代码要修改下
 

            //app.UseAuditing();//这是默认的配置!
            app.UseMiddleware<MyAuditingMiddleware>(Array.Empty<object>());

我的审计日志需求

不需要记录表的列的改动,只需要记录我标记的HttpPost的请求即可!

要记录HttpPost的时候附带了那些参数,返回结果如何!

如何查询日志?

using AutoMapper.Internal.Mappers;
using Microsoft.AspNetCore.Mvc;
using SmartShop.Controllers;
using System.Collections.Generic;
using System.Threading.Tasks;
using System;
using Volo.Abp.Auditing;
using Volo.Abp.AuditLogging;
using System.Linq;
using System.Linq.Dynamic.Core;
using Microsoft.EntityFrameworkCore;

namespace PasteSpider.HttpApi.Host.Controllers
{
    /// <summary>
    /// 公开接口,需要根据apitoken获取必要的查询
    /// </summary>
    [ApiController]
    [DisableAuditing]
    [Route("/api/app/logs/[action]")]
    public class LogsController : IBaseController
    {
        /// <summary>
        /// 
        /// </summary>
        private AuditingDbContext _logdbContext => LazyServiceProvider.LazyGetRequiredService<AuditingDbContext>();

        /// <summary>
        /// 初始化
        /// </summary>
        public LogsController()
        {

        }

        /// <summary>
        /// 获取审计日志列表
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        [HttpGet]
        [TypeFilter(typeof(RoleAttribute), Arguments = new object[] { "root", "root" })]
        public async Task<dynamic> Logs([FromQuery] InputQueryLogs input)
        {
            var _query = _logdbContext.AuditLogs
                .WhereIf(input.sdate != null && input.edate != null, x => input.sdate <= x.ExecutionTime && x.ExecutionTime < input.edate)
                .Where(x => true).WhereIf(!String.IsNullOrEmpty(input.url), x => x.Url == input.url)
                .WhereIf(!String.IsNullOrEmpty(input.client_id), x => x.ClientId == input.client_id)
                .OrderByDescending(x => x.ExecutionDuration)
                .Page(input.page, input.size);

            var datas = await _query.AsNoTracking().ToListAsync();
            if (datas != null && datas.Count > 0)
            {
                var dtos = ObjectMapper.Map<List<AuditLog>, List<AuditLogDtoModel>>(datas);
                var audilogids = datas.Select(x => x.Id).ToArray();
                if (audilogids != null && audilogids.Length > 0)
                {
                    var actions = await _logdbContext.AuditLogAction.Where(x => audilogids.Contains(x.AuditLogId)).ToListAsync();
                    if (actions != null && actions.Count > 0)
                    {
                        foreach (var item in dtos)
                        {
                            var _actions = actions.Where(x => x.AuditLogId == item.Id).ToList();
                            if (_actions != null && _actions.Count > 0)
                            {
                                item.SetLogActions(_actions);
                            }
                        }
                    }
                }
                return dtos;
            }
            else
            {
                throw new SmartShopException("没有获取到数据!", 701);
            }
        }

        /// <summary>
        /// 获取审计日志列表
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        [HttpGet]
        [TypeFilter(typeof(RoleAttribute), Arguments = new object[] { "root", "root" })]
        public async Task<dynamic> Actions([FromQuery] InputQueryLogs input)
        {
            var _query = _logdbContext.AuditLogAction
                .Where(x => true)
                .WhereIf(input.sdate != null && input.edate != null, x => input.sdate <= x.ExecutionTime && x.ExecutionTime < input.edate)
                .WhereIf(input.auditlog_id != null, x => x.AuditLogId == input.auditlog_id)
                .WhereIf(!String.IsNullOrEmpty(input.service_name), x => x.ServiceName == input.client_id)
                .WhereIf(!String.IsNullOrEmpty(input.method_name), x => x.MethodName == input.method_name)
                .OrderByDescending(x => x.ExecutionDuration)
                .Page(input.page, input.size);

            return await _query.AsNoTracking().ToListAsync();
        }

    }
    /// <summary>
    /// 查询审计日志的信息
    /// </summary>
    public class InputQueryLogs : InputSearch
    {
        /// <summary>
        /// log
        /// </summary>
        public string url { get; set; }

        /// <summary>
        /// log
        /// </summary>
        public string client_id { get; set; }

        /// <summary>
        /// 
        /// </summary>
        public DateTime? sdate { get; set; }

        /// <summary>
        /// 
        /// </summary>
        public DateTime? edate { get; set; }

        /// <summary>
        /// action 
        /// </summary>
        public Guid? auditlog_id { get; set; }

        /// <summary>
        /// action 
        /// </summary>
        public string service_name { get; set; }

        /// <summary>
        /// action 
        /// </summary>
        public string method_name { get; set; }

    }

    /// <summary>
    /// 
    /// </summary>
    public class AuditLogDtoModel : AuditLog
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="datas"></param>
        public void SetLogActions(List<AuditLogAction> datas)
        {
            base.Actions = datas;
        }
    }


}

至于前端的代码,自己搞定哈!

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值