文件内容 AuditMiddleware.cs 12 KB 12658 字符
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Options;
using PasteApeart.auditmodels;
using PasteApeart.EntityFrameworkCore.auditing;
using System.Diagnostics;
using System.Linq.Dynamic.Core;
using System.Security.Claims;
using System.Text;
using System.Text.Json;

namespace PasteApeart.HttpApi.Host.auditmodels
{

    /// <summary>
    /// 审计日志中间件
    /// </summary>
    public class AuditMiddleware : IMiddleware
    {

        private readonly ILogger<AuditMiddleware> _logger;
        private readonly IAuditLogProvider _auditLogManager;
        private readonly AuditingConfigOptions _config;
        private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="logger"></param>
        /// <param name="auditLogManager"></param>
        /// <param name="config"></param>
        /// <param name="actionDescriptorCollectionProvider"></param>
        public AuditMiddleware(ILogger<AuditMiddleware> logger, IAuditLogProvider auditLogManager, IOptions<AuditingConfigOptions> config, IActionDescriptorCollectionProvider actionDescriptorCollectionProvider)
        {

            _logger = logger;
            _auditLogManager = auditLogManager;
            _config = config.Value;
            _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        /// <param name="_next"></param>
        /// <returns></returns>
        public async Task InvokeAsync(HttpContext context, RequestDelegate _next)
        {
            if (!_config.Enable)
            {
                await _next(context);
                return;
            }

            if (IsAuditingDisabled(context))
            {
                await _next(context);
                return;
            }

            if (ShouldSkipAudit(context))
            {
                await _next(context);
                return;
            }
            var stopwatch = Stopwatch.StartNew();
            // 初始化审计日志
            var auditLog = new ApAuditLog
            {
                ExecutionTime = DateTime.UtcNow,
                Url = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}",
                HttpMethod = context.Request.Method,
                QueryString = context.Request.QueryString.ToString(),
                IpAddress = context.Connection.RemoteIpAddress?.ToString(),
                Headers = SerializeHeaders(context.Request.Headers)
            };

            // 获取用户信息
            if (context.User?.Identity?.IsAuthenticated == true)
            {
                auditLog.UserId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
            }

            if (!_config.EnableHttpGet)
            {
                if (context.Request.Method.Equals("GET", StringComparison.CurrentCultureIgnoreCase))
                {
                    await _next(context);
                    return;
                }
            }

            // 读取请求体
            if (context.Request.ContentLength > 0)
            {
                context.Request.EnableBuffering();
                auditLog.RequestBody = await ReadRequestBody(context.Request);
                context.Request.Body.Position = 0;
            }

            try
            {
                if (!_config.LogResponseBody)
                {
                    await _next(context);
                }
                else
                {
                    var originalBodyStream = context.Response.Body;
                    // 需要记录,使用缓冲流
                    using var responseBodyStream = new MemoryStream();
                    context.Response.Body = responseBodyStream;

                    try
                    {
                        await _next(context);

                        // 只记录小型响应体,避免大文件问题
                        if (responseBodyStream.Length <= 10 * 1024 * 1024) // 10MB 限制
                        {
                            responseBodyStream.Seek(0, SeekOrigin.Begin);

                            var contentType = context.Response.ContentType ?? "";
                            if (IsTextContent(contentType))
                            {
                                var responseBody = await new StreamReader(responseBodyStream, Encoding.UTF8, leaveOpen: true).ReadToEndAsync();
                                auditLog.ResponseBody = Truncate(responseBody, 5000);
                            }
                            else
                            {
                                auditLog.ResponseBody = $"[Binary] Type: {contentType}, Size: {responseBodyStream.Length} bytes";
                            }
                        }
                        else
                        {
                            auditLog.ResponseBody = $"[Large File] Size: {responseBodyStream.Length} bytes, exceeded logging limit";
                        }
                        // 复制回原始流
                        responseBodyStream.Seek(0, SeekOrigin.Begin);
                        await responseBodyStream.CopyToAsync(originalBodyStream);
                    }
                    finally
                    {
                        context.Response.Body = originalBodyStream;
                    }
                }
                stopwatch.Stop();
                auditLog.ExecutionDuration = stopwatch.ElapsedMilliseconds;
                auditLog.HttpStatusCode = context.Response.StatusCode;
            }
            catch (Exception ex)
            {
                stopwatch.Stop();
                auditLog.ExecutionDuration = stopwatch.ElapsedMilliseconds;
                auditLog.HasException = true;
                auditLog.Exception = ex.ToString();
                auditLog.HttpStatusCode = StatusCodes.Status500InternalServerError;
                throw;
            }
            finally
            {
                try
                {
                    if (context.Items[_config.AuditItemKey] is AuditCollection collectedData)
                    {
                        // 将实体变更序列化存入审计日志
                        if (collectedData.EntityChanges.Any())
                        {
                            auditLog.EntityChanges = collectedData.EntityChanges;
                        }
                        context.Items.Remove(_config.AuditItemKey);
                    }
                    // 保存审计日志
                    await _auditLogManager.InsertAsync(auditLog);
                }
                catch (Exception exl)
                {
                    Console.WriteLine(exl.ToString());
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <param name="maxLength"></param>
        /// <returns></returns>
        private string Truncate(string value, int maxLength)
        {
            if (string.IsNullOrEmpty(value)) return value;
            return value.Length <= maxLength ? value : value.Substring(0, maxLength) + "...";
        }

        /// <summary>
        /// 记录header
        /// </summary>
        /// <param name="headers"></param>
        /// <returns></returns>
        private string SerializeHeaders(IHeaderDictionary headers)
        {
            //这里可以把cookie也去掉
            var safeHeaders = headers
                .Where(h => !h.Key.Equals("Authorization", StringComparison.OrdinalIgnoreCase) && !h.Key.Equals("token", StringComparison.OrdinalIgnoreCase))
                .ToDictionary(h => h.Key, h => string.Join(",", h.Value));
            return JsonSerializer.Serialize(safeHeaders);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        private async Task<string> ReadRequestBody(HttpRequest request)
        {
            var contentType = request.ContentType ?? "";

            // 检查是否需要排除
            if (_config.ExcludedContentTypes.Any(ct => contentType.StartsWith(ct, StringComparison.OrdinalIgnoreCase)))
            {
                return $"[{contentType}] 内容未记录";
            }

            // 检查内容长度
            if (request.ContentLength > _config.MaxRequestBodySize)
            {
                return $"[请求体过大] ContentLength: {request.ContentLength} bytes,超过限制 {_config.MaxRequestBodySize} bytes";
            }

            // 对于非文本内容,不记录
            if (!IsTextContent(contentType))
            {
                return $"[非文本内容] ContentType: {contentType}";
            }

            try
            {
                request.EnableBuffering();
                request.Body.Position = 0;

                using var reader = new StreamReader(
                    request.Body,
                    Encoding.UTF8,
                    detectEncodingFromByteOrderMarks: false,
                    bufferSize: 1024,
                    leaveOpen: true);

                var result = await reader.ReadToEndAsync();

                // 限制存储长度
                if (result.Length > _config.MaxRequestBodySize)
                {
                    result = result.Substring(0, _config.MaxRequestBodySize) +
                             $"... [截断,总长度: {result.Length} characters]";
                }
                if (request.Body.CanSeek)
                {
                    request.Body.Position = 0;
                }
                return result;
            }
            catch (Exception ex)
            {
                return $"[读取请求体失败: {ex.Message}]";
            }
        }

        /// <summary>
        /// 检查是否有禁用特性
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        private bool IsAuditingDisabled(HttpContext context)
        {
            // 获取路由数据
            var routeData = context.GetRouteData();
            var controllerName = routeData.Values["controller"]?.ToString();
            var actionName = routeData.Values["action"]?.ToString();

            if (string.IsNullOrEmpty(controllerName) || string.IsNullOrEmpty(actionName))
                return false;

            // 从所有 Action 描述中查找匹配的
            foreach (var descriptor in _actionDescriptorCollectionProvider.ActionDescriptors.Items)
            {
                if (descriptor is ControllerActionDescriptor controllerDescriptor)
                {
                    if (controllerDescriptor.ControllerName.Equals(controllerName, StringComparison.OrdinalIgnoreCase) &&
                        controllerDescriptor.ActionName.Equals(actionName, StringComparison.OrdinalIgnoreCase))
                    {
                        // 检查 Action 特性
                        var actionDisabled = controllerDescriptor.MethodInfo
                            .GetCustomAttributes(typeof(DisableAuditingAttribute), true)
                            .Any();

                        // 检查 Controller 特性
                        var controllerDisabled = controllerDescriptor.ControllerTypeInfo
                            .GetCustomAttributes(typeof(DisableAuditingAttribute), true)
                            .Any();
                        //这里有问题 如果Controller是禁用的 Action是启用的那么应该启用
                        return actionDisabled || controllerDisabled;
                    }
                }
            }
            return false;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="contentType"></param>
        /// <returns></returns>
        private bool IsTextContent(string contentType)
        {
            var textTypes = new[]
            {
                "application/json",
                "application/xml",
                "text/",
                "application/x-www-form-urlencoded"
            };
            return textTypes.Any(tt => contentType.StartsWith(tt, StringComparison.OrdinalIgnoreCase));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        private bool ShouldSkipAudit(HttpContext context)
        {
            var path = context.Request.Path.Value?.ToLower();
            if (!String.IsNullOrEmpty(path))
            {
                if (_config.IgnoredPaths?.Count > 0)
                {
                    if (_config.IgnoredPaths.Contains(path))
                    {
                        return true;
                    }
                }
            }
            return false;
        }

    }




}
​ ​