@ -0,0 +1,397 @@
using Microsoft.AspNetCore.Mvc ;
using Microsoft.AspNetCore.Mvc.Filters ;
using Microsoft.AspNetCore.Http ;
using Microsoft.Extensions.Logging ;
using QYZH.InteractiveMagazine.IService ;
using QYZH.InteractiveMagazine.Models.Dto ;
using System.Collections ;
using System.Diagnostics ;
using System.Reflection ;
using System.Security.Claims ;
using System.Text.Json ;
using System.Text.Json.Serialization ;
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware ;
/// <summary>
/// 操作日志过滤器
/// </summary>
public class OperationLogActionFilter (
IOperationLogService operationLogService ,
ILogger < OperationLogActionFilter > logger ) : IAsyncActionFilter
{
private static readonly HashSet < string > SensitiveNames = new ( StringComparer . OrdinalIgnoreCase )
{
"password" ,
"oldPassword" ,
"newPassword" ,
"token" ,
"secret" ,
"authorization"
} ;
private static readonly JsonSerializerOptions JsonOptions = new ( )
{
PropertyNamingPolicy = JsonNamingPolicy . CamelCase ,
ReferenceHandler = ReferenceHandler . IgnoreCycles
} ;
/// <summary>
/// 执行操作日志过滤器
/// </summary>
/// <param name="context">Action 执行上下文</param>
/// <param name="next">后续执行委托</param>
public async Task OnActionExecutionAsync ( ActionExecutingContext context , ActionExecutionDelegate next )
{
var attribute = context . ActionDescriptor . EndpointMetadata
. OfType < OperationLogAttribute > ( )
. FirstOrDefault ( ) ;
if ( attribute = = null )
{
await next ( ) ;
return ;
}
var stopwatch = Stopwatch . StartNew ( ) ;
var executedContext = await next ( ) ;
stopwatch . Stop ( ) ;
if ( executedContext . Exception ! = null & & ! executedContext . ExceptionHandled )
{
return ;
}
if ( ! IsSuccessResult ( executedContext . Result , context . HttpContext . Response . StatusCode ) )
{
return ;
}
try
{
var operatorId = GetOperatorId ( context ) ;
if ( ! operatorId . HasValue )
{
return ;
}
var operatorName = GetOperatorName ( context ) ;
var responseValue = GetResponseValue ( executedContext . Result ) ;
var responseResult = GetResponseResult ( responseValue ) ;
var targetId = ResolveTargetId ( attribute , context , responseResult , operatorId . Value ) ;
var targetName = ResolveTargetName ( attribute , context , responseResult ) ;
var detail = BuildDetail ( attribute , context , responseValue , stopwatch . ElapsedMilliseconds ) ;
await operationLogService . LogAsync ( new OperationLogRecordInput
{
OperatorId = operatorId . Value ,
OperatorName = operatorName ,
ActionType = attribute . ActionType ,
TargetType = attribute . TargetType ,
TargetId = targetId ,
TargetName = targetName ,
Detail = detail ,
IpAddress = GetClientIp ( context )
} ) ;
}
catch ( Exception ex )
{
logger . LogError ( ex , "自动记录操作日志失败, Path: {Path}" , context . HttpContext . Request . Path ) ;
}
}
private static bool IsSuccessResult ( IActionResult ? result , int responseStatusCode )
{
var responseValue = GetResponseValue ( result ) ;
if ( responseValue is BaseResponse response )
{
return response . isSuccess ;
}
if ( result is ObjectResult objectResult & & objectResult . StatusCode . HasValue )
{
return IsSuccessStatusCode ( objectResult . StatusCode . Value ) ;
}
if ( result is StatusCodeResult statusCodeResult )
{
return IsSuccessStatusCode ( statusCodeResult . StatusCode ) ;
}
return IsSuccessStatusCode ( responseStatusCode = = 0 ? StatusCodes . Status200OK : responseStatusCode ) ;
}
private static bool IsSuccessStatusCode ( int statusCode )
{
return statusCode > = StatusCodes . Status200OK & & statusCode < StatusCodes . Status300MultipleChoices ;
}
private static long? GetOperatorId ( ActionExecutingContext context )
{
var value = context . HttpContext . User . Claims
. FirstOrDefault ( c = > c . Type = = ClaimTypes . NameIdentifier )
? . Value ;
return long . TryParse ( value , out var operatorId ) ? operatorId : null ;
}
private static string GetOperatorName ( ActionExecutingContext context )
{
return context . HttpContext . User . Claims
. FirstOrDefault ( c = > c . Type = = ClaimTypes . Name )
? . Value ? ? string . Empty ;
}
private static string? GetClientIp ( ActionExecutingContext context )
{
var request = context . HttpContext . Request ;
var forwardedFor = request . Headers [ "X-Forwarded-For" ] . FirstOrDefault ( ) ;
if ( ! string . IsNullOrWhiteSpace ( forwardedFor ) )
{
return forwardedFor . Split ( ',' ) [ 0 ] . Trim ( ) ;
}
var realIp = request . Headers [ "X-Real-IP" ] . FirstOrDefault ( ) ;
if ( ! string . IsNullOrWhiteSpace ( realIp ) )
{
return realIp ;
}
return context . HttpContext . Connection . RemoteIpAddress ? . ToString ( ) ;
}
private static long ResolveTargetId ( OperationLogAttribute attribute , ActionExecutingContext context , object? responseResult , long operatorId )
{
if ( attribute . UseOperatorAsTargetId )
{
return operatorId ;
}
if ( TryGetLongRouteValue ( context , attribute . TargetIdRouteKey , out var routeId ) )
{
return routeId ;
}
if ( TryGetLongArgumentValue ( context , attribute . TargetIdArgumentName , out var argumentId ) )
{
return argumentId ;
}
foreach ( var key in new [ ] { "id" , "userId" , "adminUserId" , "roleId" , "taskId" , "recordId" } )
{
if ( TryGetLongRouteValue ( context , key , out routeId ) | | TryGetLongArgumentValue ( context , key , out argumentId ) )
{
return routeId ! = 0 ? routeId : argumentId ;
}
}
if ( TryConvertToLong ( responseResult , out var responseId ) )
{
return responseId ;
}
return TryGetLongPropertyValue ( responseResult , "Id" , out responseId ) ? responseId : 0 ;
}
private static string? ResolveTargetName ( OperationLogAttribute attribute , ActionExecutingContext context , object? responseResult )
{
if ( ! string . IsNullOrWhiteSpace ( attribute . TargetNameArgumentName )
& & context . ActionArguments . TryGetValue ( attribute . TargetNameArgumentName , out var nameArgument ) )
{
if ( nameArgument is string name )
{
return name ;
}
if ( ! string . IsNullOrWhiteSpace ( attribute . TargetNameProperty )
& & TryGetStringPropertyValue ( nameArgument , attribute . TargetNameProperty , out name ) )
{
return name ;
}
}
foreach ( var value in context . ActionArguments . Values )
{
if ( ! string . IsNullOrWhiteSpace ( attribute . TargetNameProperty )
& & TryGetStringPropertyValue ( value , attribute . TargetNameProperty , out var configuredName ) )
{
return configuredName ;
}
if ( TryGetStringPropertyValue ( value , "Name" , out var name )
| | TryGetStringPropertyValue ( value , "Title" , out name ) )
{
return name ;
}
}
if ( ! string . IsNullOrWhiteSpace ( attribute . TargetNameProperty )
& & TryGetStringPropertyValue ( responseResult , attribute . TargetNameProperty , out var responseName ) )
{
return responseName ;
}
return TryGetStringPropertyValue ( responseResult , "Name" , out var defaultName )
| | TryGetStringPropertyValue ( responseResult , "Title" , out defaultName )
? defaultName
: null ;
}
private static string BuildDetail ( OperationLogAttribute attribute , ActionExecutingContext context , object? responseValue , long elapsedMilliseconds )
{
var detail = new Dictionary < string , object? >
{
["method"] = context . HttpContext . Request . Method ,
["path"] = context . HttpContext . Request . Path . Value ,
["routeValues"] = context . RouteData . Values . ToDictionary ( k = > k . Key , v = > v . Value ? . ToString ( ) ) ,
["arguments"] = attribute . LogArguments ? SanitizeValue ( context . ActionArguments , 0 ) : null ,
["responseMessage"] = GetResponseMessage ( responseValue ) ,
["elapsedMilliseconds"] = elapsedMilliseconds
} ;
return JsonSerializer . Serialize ( detail , JsonOptions ) ;
}
private static string? GetResponseMessage ( object? responseValue )
{
return responseValue is BaseResponse response ? response . message : null ;
}
private static object? GetResponseValue ( IActionResult ? result )
{
return result switch
{
ObjectResult objectResult = > objectResult . Value ,
JsonResult jsonResult = > jsonResult . Value ,
_ = > null
} ;
}
private static object? GetResponseResult ( object? responseValue )
{
if ( responseValue = = null )
{
return null ;
}
return responseValue . GetType ( )
. GetProperty ( "result" , BindingFlags . Instance | BindingFlags . Public | BindingFlags . IgnoreCase )
? . GetValue ( responseValue ) ;
}
private static object? SanitizeValue ( object? value , int depth )
{
if ( value = = null )
{
return null ;
}
if ( depth > 4 )
{
return value . ToString ( ) ;
}
var type = value . GetType ( ) ;
if ( type . IsPrimitive | | value is string or decimal or DateTime or DateTimeOffset or Guid | | type . IsEnum )
{
return value ;
}
if ( value is IDictionary dictionary )
{
var result = new Dictionary < string , object? > ( ) ;
foreach ( DictionaryEntry item in dictionary )
{
var key = item . Key ? . ToString ( ) ? ? string . Empty ;
result [ key ] = SensitiveNames . Contains ( key ) ? "***" : SanitizeValue ( item . Value , depth + 1 ) ;
}
return result ;
}
if ( value is IEnumerable enumerable & & value is not string )
{
return enumerable . Cast < object? > ( )
. Take ( 20 )
. Select ( item = > SanitizeValue ( item , depth + 1 ) )
. ToList ( ) ;
}
return type . GetProperties ( BindingFlags . Instance | BindingFlags . Public )
. Where ( p = > p . GetIndexParameters ( ) . Length = = 0 )
. ToDictionary (
p = > p . Name ,
p = > SensitiveNames . Contains ( p . Name ) ? "***" : SanitizeValue ( p . GetValue ( value ) , depth + 1 ) ) ;
}
private static bool TryGetLongRouteValue ( ActionExecutingContext context , string? key , out long value )
{
value = 0 ;
if ( string . IsNullOrWhiteSpace ( key ) | | ! context . RouteData . Values . TryGetValue ( key , out var routeValue ) )
{
return false ;
}
return long . TryParse ( routeValue ? . ToString ( ) , out value ) ;
}
private static bool TryGetLongArgumentValue ( ActionExecutingContext context , string? key , out long value )
{
value = 0 ;
if ( string . IsNullOrWhiteSpace ( key ) | | ! context . ActionArguments . TryGetValue ( key , out var argumentValue ) )
{
return false ;
}
if ( TryConvertToLong ( argumentValue , out value ) )
{
return true ;
}
return TryGetLongPropertyValue ( argumentValue , "Id" , out value ) ;
}
private static bool TryGetLongPropertyValue ( object? source , string propertyName , out long value )
{
value = 0 ;
if ( source = = null )
{
return false ;
}
var property = source . GetType ( ) . GetProperty ( propertyName , BindingFlags . Instance | BindingFlags . Public | BindingFlags . IgnoreCase ) ;
return property ! = null & & TryConvertToLong ( property . GetValue ( source ) , out value ) ;
}
private static bool TryConvertToLong ( object? source , out long value )
{
value = 0 ;
return source switch
{
long longValue = > SetValue ( longValue , out value ) ,
int intValue = > SetValue ( intValue , out value ) ,
string stringValue = > long . TryParse ( stringValue , out value ) ,
_ = > long . TryParse ( source ? . ToString ( ) , out value )
} ;
}
private static bool TryGetStringPropertyValue ( object? source , string propertyName , out string? value )
{
value = null ;
if ( source = = null )
{
return false ;
}
var property = source . GetType ( ) . GetProperty ( propertyName , BindingFlags . Instance | BindingFlags . Public | BindingFlags . IgnoreCase ) ;
value = property ? . GetValue ( source ) ? . ToString ( ) ;
return ! string . IsNullOrWhiteSpace ( value ) ;
}
private static bool SetValue ( long source , out long value )
{
value = source ;
return true ;
}
}