| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657 |
- using OASystem.Domain.AesEncryption;
- using OASystem.Domain.Attributes;
- using OASystem.Domain.Entities.Customer;
- using UAParser;
- using static OASystem.API.OAMethodLib.JWTHelper;
- using System.Text;
- using Microsoft.Extensions.Logging;
- using Microsoft.AspNetCore.Http;
- using Newtonsoft.Json;
- using Newtonsoft.Json.Linq;
- namespace OASystem.API.Middlewares
- {
- /// <summary>
- /// 指定API操作记录信息
- /// </summary>
- public class RecordAPIOperationMiddleware
- {
- private readonly RequestDelegate _next;
- private readonly HttpClient _httpClient;
- private readonly IConfiguration _config;
- private readonly ILogger<RecordAPIOperationMiddleware> _logger;
- private readonly IServiceProvider _serviceProvider;
- /// <summary>
- /// 初始化
- /// </summary>
- public RecordAPIOperationMiddleware(
- RequestDelegate next,
- IConfiguration config,
- IHttpClientFactory httpClientFactory,
- ILogger<RecordAPIOperationMiddleware> logger,
- IServiceProvider serviceProvider)
- {
- _next = next;
- _config = config;
- _logger = logger;
- _serviceProvider = serviceProvider;
- _httpClient = httpClientFactory.CreateClient();
- _httpClient.Timeout = TimeSpan.FromSeconds(5);
- }
- public async Task InvokeAsync(HttpContext context)
- {
- // 跳过 OPTIONS 请求(CORS 预检请求)
- if (context.Request.Method.Equals(HttpMethods.Options, StringComparison.OrdinalIgnoreCase))
- {
- await _next(context);
- return;
- }
- // 跳过静态文件请求
- if (IsStaticFileRequest(context.Request.Path))
- {
- await _next(context);
- return;
- }
- // 启用请求体流的缓冲,允许多次读取
- context.Request.EnableBuffering();
- // 读取请求体内容
- var requestBodyText = await ReadRequestBody(context.Request);
- // 检查控制器方法是否使用了自定义属性
- var endpoint = context.GetEndpoint();
- var apiLogAttribute = endpoint?.Metadata?.GetMetadata<ApiLogAttribute>();
- if (apiLogAttribute != null)
- {
- var startTime = DateTime.UtcNow;
- int portType = 1, userId = 0, id = 0, status = 0;
- string updatePreData = string.Empty, updateBefData = string.Empty;
- // 获取用户ID和其他参数
- using (var scope = _serviceProvider.CreateScope())
- {
- var sqlSugar = scope.ServiceProvider.GetRequiredService<SqlSugarClient>();
- try
- {
- userId = await ReadToken(context);
- if (!string.IsNullOrEmpty(requestBodyText))
- {
- var requestBodyJson = JsonConvert.DeserializeObject<Dictionary<string, object>>(requestBodyText);
- if (requestBodyJson != null)
- {
- // 提取参数
- if (requestBodyJson.TryGetValue("portType", out var param1Obj) && param1Obj != null)
- {
- int.TryParse(param1Obj.ToString(), out portType);
- }
- if (requestBodyJson.TryGetValue("id", out var param5Obj) && param5Obj != null)
- {
- int.TryParse(param5Obj.ToString(), out id);
- }
- if (requestBodyJson.TryGetValue("status", out var param6Obj) && param6Obj != null)
- {
- int.TryParse(param6Obj.ToString(), out status);
- }
- // 用户Id处理
- if (userId < 1)
- {
- if (apiLogAttribute.OperationEnum == OperationEnum.Login)
- {
- var number = requestBodyJson.TryGetValue("number", out var numberObj) ? numberObj?.ToString() : null;
- if (!string.IsNullOrEmpty(number))
- {
- var info = await sqlSugar.Queryable<Sys_Users>()
- .Where(x => x.IsDel == 0 && x.Number.Equals(number))
- .FirstAsync();
- userId = info?.Id ?? 0;
- }
- }
- else
- {
- userId = ParseUserIdFromParams(requestBodyJson);
- }
- }
- // 根据status判断操作类型
- if (status > 0)
- {
- if (status == 1)
- apiLogAttribute.OperationEnum = OperationEnum.Add;
- else if (status == 2)
- {
- apiLogAttribute.OperationEnum = OperationEnum.Edit;
- if (id > 0)
- {
- updatePreData = await TableInfoToJson(sqlSugar, apiLogAttribute.TableName, id);
- }
- }
- }
- // 根据id判断操作类型
- else if (id > 0 && apiLogAttribute.OperationEnum != OperationEnum.Del)
- {
- apiLogAttribute.OperationEnum = OperationEnum.Edit;
- updatePreData = await TableInfoToJson(sqlSugar, apiLogAttribute.TableName, id);
- }
- else if (apiLogAttribute.OperationEnum != OperationEnum.Del && id < 1)
- {
- apiLogAttribute.OperationEnum = OperationEnum.Add;
- }
- }
- }
- }
- catch (JsonException)
- {
- _logger.LogDebug("JSON解析失败,可能请求体不是JSON格式");
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "解析请求参数时发生错误");
- }
- }
- // 保存原始响应体流
- var originalResponseBody = context.Response.Body;
- // 创建一个新的内存流来捕获响应体
- using var responseMemoryStream = new MemoryStream();
- context.Response.Body = responseMemoryStream;
- // 调用下一个中间件
- await _next(context);
- // 重置响应体流的位置
- responseMemoryStream.Position = 0;
- // 读取响应体内容
- var responseBodyText = await ReadResponseBody(responseMemoryStream);
- // 将响应体内容写回原始响应体流
- await responseMemoryStream.CopyToAsync(originalResponseBody);
- // 修改后数据查询
- if (status == 2 && id > 0)
- {
- using (var scope = _serviceProvider.CreateScope())
- {
- var sqlSugar = scope.ServiceProvider.GetRequiredService<SqlSugarClient>();
- updateBefData = await TableInfoToJson(sqlSugar, apiLogAttribute.TableName, id);
- }
- }
- else if (apiLogAttribute.OperationEnum == OperationEnum.Edit && id > 0)
- {
- using (var scope = _serviceProvider.CreateScope())
- {
- var sqlSugar = scope.ServiceProvider.GetRequiredService<SqlSugarClient>();
- updateBefData = await TableInfoToJson(sqlSugar, apiLogAttribute.TableName, id);
- }
- }
- // 获取IP信息和设备信息
- string remoteIp = GetClientIp(context);
- string location = await GetIpLocationSafe(remoteIp);
- var (deviceType, browser, os) = GetDeviceInfo(context);
- // 记录请求结束时间
- var endTime = DateTime.UtcNow;
- // 计算耗时
- var duration = (long)(endTime - startTime).TotalMilliseconds;
- // 截断过长的文本
- var truncatedRequestBody = TruncateString(requestBodyText, 4000);
- var truncatedResponseBody = TruncateString(responseBodyText, 4000);
- var truncatedPreData = TruncateString(updatePreData, 4000);
- var truncatedBefData = TruncateString(updateBefData, 4000);
- var logInfo = new Crm_TableOperationRecord()
- {
- TableName = apiLogAttribute.TableName,
- PortType = portType,
- OperationItem = apiLogAttribute.OperationEnum,
- DataId = id,
- RequestUrl = context.Request.Path + context.Request.QueryString,
- RemoteIp = remoteIp,
- Location = location,
- RequestParam = !string.IsNullOrEmpty(truncatedRequestBody) ? JsonConvert.SerializeObject(truncatedRequestBody) : null,
- ReturnResult = !string.IsNullOrEmpty(truncatedResponseBody) ? JsonConvert.SerializeObject(truncatedResponseBody) : null,
- Elapsed = duration,
- Status = context.Response.StatusCode.ToString(),
- CreateUserId = userId,
- UpdatePreData = truncatedPreData,
- UpdateBefData = truncatedBefData,
- Browser = browser,
- Os = os,
- DeviceType = deviceType,
- CreateTime = DateTime.Now
- };
- // 异步记录日志,不阻塞响应
- _ = Task.Run(async () =>
- {
- try
- {
- // 在异步任务中创建新的作用域
- using var logScope = _serviceProvider.CreateScope();
- var logSqlSugar = logScope.ServiceProvider.GetRequiredService<SqlSugarClient>();
- // 设置较短的超时时间
- logSqlSugar.Ado.CommandTimeOut = 3;
- // 存储到数据库
- await logSqlSugar.Insertable(logInfo).ExecuteCommandAsync();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "异步记录操作日志失败");
- // 降级:写入文件日志
- try
- {
- await WriteLogToFile(apiLogAttribute, requestBodyText, responseBodyText, userId, ex.Message);
- }
- catch (Exception fileEx)
- {
- _logger.LogError(fileEx, "写入文件日志失败");
- }
- }
- });
- }
- else
- {
- await _next(context);
- }
- }
- /// <summary>
- /// 从请求参数中解析用户ID
- /// </summary>
- private int ParseUserIdFromParams(Dictionary<string, object> requestBodyJson)
- {
- var userIdKeys = new[] { "userId", "currUserId", "createUserId", "operationUserId", "deleteUserId" };
- foreach (var key in userIdKeys)
- {
- if (requestBodyJson.TryGetValue(key, out var value) && value != null)
- {
- if (int.TryParse(value.ToString(), out var parsedUserId) && parsedUserId > 0)
- {
- return parsedUserId;
- }
- }
- }
- return 0;
- }
- /// <summary>
- /// 获取表数据JSON
- /// </summary>
- private async Task<string> TableInfoToJson(SqlSugarClient sqlSugar, string tableName, int id)
- {
- if (sqlSugar.DbMaintenance.IsAnyTable(tableName))
- {
- try
- {
- string jsonLabel = string.Empty;
- if (tableName.Equals("Crm_NewClientData"))
- {
- var info = await sqlSugar.Queryable<Crm_NewClientData>()
- .Where(x => x.Id == id)
- .FirstAsync();
- if (info != null)
- {
- EncryptionProcessor.DecryptProperties(info);
- if (info != null)
- jsonLabel = JsonConvert.SerializeObject(info);
- }
- }
- else
- {
- var sql = $"SELECT * FROM {tableName} WHERE Id = {id}";
- var info = await sqlSugar.SqlQueryable<dynamic>(sql).FirstAsync();
- if (info != null)
- jsonLabel = JsonConvert.SerializeObject(info);
- }
- return jsonLabel;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, $"获取表数据失败: {tableName}, Id: {id}");
- return string.Empty;
- }
- }
- return string.Empty;
- }
- /// <summary>
- /// 是否是静态文件请求
- /// </summary>
- private bool IsStaticFileRequest(PathString path)
- {
- try
- {
- var staticExtensions = new[] {
- ".css", ".js", ".png", ".jpg", ".jpeg", ".gif",
- ".ico", ".svg", ".woff", ".woff2", ".ttf", ".eot",
- ".mp4", ".mp3", ".avi", ".mov", ".wmv", ".flv",
- ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
- ".zip", ".rar", ".7z", ".tar", ".gz"
- };
- var pathString = path.Value?.ToLower() ?? string.Empty;
- return staticExtensions.Any(ext => pathString.EndsWith(ext.ToLower(), StringComparison.OrdinalIgnoreCase));
- }
- catch
- {
- return false;
- }
- }
- /// <summary>
- /// 读取请求体
- /// </summary>
- private async Task<string> ReadRequestBody(HttpRequest request)
- {
- try
- {
- request.Body.Position = 0;
- using var reader = new StreamReader(
- request.Body,
- Encoding.UTF8,
- detectEncodingFromByteOrderMarks: false,
- bufferSize: 8192,
- leaveOpen: true
- );
- var body = await reader.ReadToEndAsync();
- request.Body.Position = 0;
- return body;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "读取请求体失败");
- return string.Empty;
- }
- }
- /// <summary>
- /// 读取响应体
- /// </summary>
- private async Task<string> ReadResponseBody(Stream stream)
- {
- try
- {
- using var reader = new StreamReader(
- stream,
- Encoding.UTF8,
- detectEncodingFromByteOrderMarks: false,
- bufferSize: 8192,
- leaveOpen: true
- );
- var body = await reader.ReadToEndAsync();
- stream.Position = 0;
- return body;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "读取响应体失败");
- return string.Empty;
- }
- }
- /// <summary>
- /// 读取Token
- /// </summary>
- private async Task<int> ReadToken(HttpContext context)
- {
- try
- {
- var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
- if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
- {
- var authInfo = JwtHelper.SerializeJwt(authHeader);
- if (authInfo != null)
- return authInfo.UserId;
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "读取Token失败");
- }
- return 0;
- }
- /// <summary>
- /// 获取客户端IP
- /// </summary>
- private string GetClientIp(HttpContext context)
- {
- try
- {
- // 优先从X-Forwarded-For获取
- if (context.Request.Headers.ContainsKey("X-Forwarded-For"))
- {
- var xForwardedFor = context.Request.Headers["X-Forwarded-For"].ToString();
- if (!string.IsNullOrEmpty(xForwardedFor))
- {
- var ips = xForwardedFor.Split(',', StringSplitOptions.RemoveEmptyEntries);
- if (ips.Length > 0)
- return ips[0].Trim();
- }
- }
- // 其次从X-Real-IP获取
- if (context.Request.Headers.ContainsKey("X-Real-IP"))
- {
- var xRealIp = context.Request.Headers["X-Real-IP"].ToString();
- if (!string.IsNullOrEmpty(xRealIp))
- return xRealIp.Trim();
- }
- // 最后从RemoteIpAddress获取
- var remoteIp = context.Connection.RemoteIpAddress?.ToString();
- if (!string.IsNullOrEmpty(remoteIp))
- {
- // 处理IPv6映射的IPv4地址
- if (remoteIp.StartsWith("::ffff:"))
- return remoteIp.Substring(7);
- return remoteIp;
- }
- return "Unknown";
- }
- catch
- {
- return "Unknown";
- }
- }
- /// <summary>
- /// 安全获取IP位置
- /// </summary>
- private async Task<string> GetIpLocationSafe(string ip)
- {
- if (string.IsNullOrWhiteSpace(ip) || ip == "Unknown" || ip == "::1" || ip == "127.0.0.1")
- return "本地";
- try
- {
- // IPv6地址
- if (ip.Contains(":"))
- return "IPv6";
- // 检查是否是内网地址
- if (IsPrivateIp(ip))
- return "内网";
- // 可以在这里调用IP查询API
- // 但为了性能,暂时返回未知
- return "未知";
- }
- catch (Exception ex)
- {
- _logger.LogWarning(ex, $"获取IP位置失败: {ip}");
- return "未知";
- }
- }
- /// <summary>
- /// 检查是否是内网IP
- /// </summary>
- private bool IsPrivateIp(string ip)
- {
- try
- {
- if (string.IsNullOrWhiteSpace(ip))
- return false;
- // 常见的私有IP地址段
- if (ip.StartsWith("10.") ||
- ip.StartsWith("192.168.") ||
- ip.StartsWith("172.16.") ||
- ip.StartsWith("172.17.") ||
- ip.StartsWith("172.18.") ||
- ip.StartsWith("172.19.") ||
- ip.StartsWith("172.20.") ||
- ip.StartsWith("172.21.") ||
- ip.StartsWith("172.22.") ||
- ip.StartsWith("172.23.") ||
- ip.StartsWith("172.24.") ||
- ip.StartsWith("172.25.") ||
- ip.StartsWith("172.26.") ||
- ip.StartsWith("172.27.") ||
- ip.StartsWith("172.28.") ||
- ip.StartsWith("172.29.") ||
- ip.StartsWith("172.30.") ||
- ip.StartsWith("172.31."))
- {
- return true;
- }
- return false;
- }
- catch
- {
- return false;
- }
- }
- /// <summary>
- /// 获取设备信息
- /// </summary>
- private (string deviceType, string browser, string os) GetDeviceInfo(HttpContext context)
- {
- try
- {
- var userAgent = context.Request.Headers["User-Agent"].FirstOrDefault();
- if (string.IsNullOrEmpty(userAgent))
- return ("Unknown", "Unknown", "Unknown");
- var parser = Parser.GetDefault();
- var client = parser.Parse(userAgent);
- // 提取浏览器信息
- var browser = client.UA.Family;
- var browserVersion = new List<string>();
- if (!string.IsNullOrEmpty(client.UA.Major))
- browserVersion.Add(client.UA.Major);
- if (!string.IsNullOrEmpty(client.UA.Minor))
- browserVersion.Add(client.UA.Minor);
- if (!string.IsNullOrEmpty(client.UA.Patch))
- browserVersion.Add(client.UA.Patch);
- if (browserVersion.Any())
- browser += " " + string.Join(".", browserVersion);
- // 提取操作系统信息
- var os = client.OS.Family;
- var osVersion = new List<string>();
- if (!string.IsNullOrEmpty(client.OS.Major))
- osVersion.Add(client.OS.Major);
- if (!string.IsNullOrEmpty(client.OS.Minor))
- osVersion.Add(client.OS.Minor);
- if (!string.IsNullOrEmpty(client.OS.Patch))
- osVersion.Add(client.OS.Patch);
- if (!string.IsNullOrEmpty(client.OS.PatchMinor))
- osVersion.Add(client.OS.PatchMinor);
- if (osVersion.Any())
- os += " " + string.Join(".", osVersion);
- // 提取设备信息
- var deviceType = client.Device.Family;
- if (string.Equals(deviceType, "Other", StringComparison.OrdinalIgnoreCase))
- {
- // 根据userAgent判断设备类型
- var ua = userAgent.ToLower();
- if (ua.Contains("mobile") || ua.Contains("android") || ua.Contains("iphone") || ua.Contains("ipad"))
- deviceType = "Mobile";
- else
- deviceType = "Desktop";
- }
- return (deviceType, browser, os);
- }
- catch (Exception ex)
- {
- _logger.LogDebug(ex, "解析设备信息失败");
- return ("Unknown", "Unknown", "Unknown");
- }
- }
- /// <summary>
- /// 截断字符串
- /// </summary>
- private string TruncateString(string input, int maxLength)
- {
- if (string.IsNullOrEmpty(input) || input.Length <= maxLength)
- return input;
- return input.Substring(0, maxLength) + "...[已截断]";
- }
- /// <summary>
- /// 将日志写入文件
- /// </summary>
- private async Task WriteLogToFile(ApiLogAttribute apiLogAttribute, string requestBody, string responseBody, int userId, string error)
- {
- try
- {
- var logDir = Path.Combine(Directory.GetCurrentDirectory(), "Logs", "OperationLogs");
- Directory.CreateDirectory(logDir);
- var logFile = Path.Combine(logDir, $"operation_fallback_{DateTime.Now:yyyyMMdd}.log");
- var logEntry = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} | " +
- $"Table: {apiLogAttribute.TableName} | " +
- $"Operation: {apiLogAttribute.OperationEnum} | " +
- $"User: {userId} | " +
- $"Request: {TruncateString(requestBody, 500)} | " +
- $"Response: {TruncateString(responseBody, 500)} | " +
- $"Error: {error}" +
- Environment.NewLine;
- await File.AppendAllTextAsync(logFile, logEntry);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "写入文件日志失败");
- }
- }
- }
- }
|