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;
}
}
}
至于前端的代码,自己搞定哈!