RecordAPIOperationMiddleware.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  1. using OASystem.Domain.AesEncryption;
  2. using OASystem.Domain.Attributes;
  3. using OASystem.Domain.Entities.Customer;
  4. using UAParser;
  5. using static OASystem.API.OAMethodLib.JWTHelper;
  6. using System.Text;
  7. using Microsoft.Extensions.Logging;
  8. using Microsoft.AspNetCore.Http;
  9. using Newtonsoft.Json;
  10. using Newtonsoft.Json.Linq;
  11. namespace OASystem.API.Middlewares
  12. {
  13. /// <summary>
  14. /// 指定API操作记录信息
  15. /// </summary>
  16. public class RecordAPIOperationMiddleware
  17. {
  18. private readonly RequestDelegate _next;
  19. private readonly HttpClient _httpClient;
  20. private readonly IConfiguration _config;
  21. private readonly ILogger<RecordAPIOperationMiddleware> _logger;
  22. private readonly IServiceProvider _serviceProvider;
  23. /// <summary>
  24. /// 初始化
  25. /// </summary>
  26. public RecordAPIOperationMiddleware(
  27. RequestDelegate next,
  28. IConfiguration config,
  29. IHttpClientFactory httpClientFactory,
  30. ILogger<RecordAPIOperationMiddleware> logger,
  31. IServiceProvider serviceProvider)
  32. {
  33. _next = next;
  34. _config = config;
  35. _logger = logger;
  36. _serviceProvider = serviceProvider;
  37. _httpClient = httpClientFactory.CreateClient();
  38. _httpClient.Timeout = TimeSpan.FromSeconds(5);
  39. }
  40. public async Task InvokeAsync(HttpContext context)
  41. {
  42. // 跳过 OPTIONS 请求(CORS 预检请求)
  43. if (context.Request.Method.Equals(HttpMethods.Options, StringComparison.OrdinalIgnoreCase))
  44. {
  45. await _next(context);
  46. return;
  47. }
  48. // 跳过静态文件请求
  49. if (IsStaticFileRequest(context.Request.Path))
  50. {
  51. await _next(context);
  52. return;
  53. }
  54. // 启用请求体流的缓冲,允许多次读取
  55. context.Request.EnableBuffering();
  56. // 读取请求体内容
  57. var requestBodyText = await ReadRequestBody(context.Request);
  58. // 检查控制器方法是否使用了自定义属性
  59. var endpoint = context.GetEndpoint();
  60. var apiLogAttribute = endpoint?.Metadata?.GetMetadata<ApiLogAttribute>();
  61. if (apiLogAttribute != null)
  62. {
  63. var startTime = DateTime.UtcNow;
  64. int portType = 1, userId = 0, id = 0, status = 0;
  65. string updatePreData = string.Empty, updateBefData = string.Empty;
  66. // 获取用户ID和其他参数
  67. using (var scope = _serviceProvider.CreateScope())
  68. {
  69. var sqlSugar = scope.ServiceProvider.GetRequiredService<SqlSugarClient>();
  70. try
  71. {
  72. userId = await ReadToken(context);
  73. if (!string.IsNullOrEmpty(requestBodyText))
  74. {
  75. var requestBodyJson = JsonConvert.DeserializeObject<Dictionary<string, object>>(requestBodyText);
  76. if (requestBodyJson != null)
  77. {
  78. // 提取参数
  79. if (requestBodyJson.TryGetValue("portType", out var param1Obj) && param1Obj != null)
  80. {
  81. int.TryParse(param1Obj.ToString(), out portType);
  82. }
  83. if (requestBodyJson.TryGetValue("id", out var param5Obj) && param5Obj != null)
  84. {
  85. int.TryParse(param5Obj.ToString(), out id);
  86. }
  87. if (requestBodyJson.TryGetValue("status", out var param6Obj) && param6Obj != null)
  88. {
  89. int.TryParse(param6Obj.ToString(), out status);
  90. }
  91. // 用户Id处理
  92. if (userId < 1)
  93. {
  94. if (apiLogAttribute.OperationEnum == OperationEnum.Login)
  95. {
  96. var number = requestBodyJson.TryGetValue("number", out var numberObj) ? numberObj?.ToString() : null;
  97. if (!string.IsNullOrEmpty(number))
  98. {
  99. var info = await sqlSugar.Queryable<Sys_Users>()
  100. .Where(x => x.IsDel == 0 && x.Number.Equals(number))
  101. .FirstAsync();
  102. userId = info?.Id ?? 0;
  103. }
  104. }
  105. else
  106. {
  107. userId = ParseUserIdFromParams(requestBodyJson);
  108. }
  109. }
  110. // 根据status判断操作类型
  111. if (status > 0)
  112. {
  113. if (status == 1)
  114. apiLogAttribute.OperationEnum = OperationEnum.Add;
  115. else if (status == 2)
  116. {
  117. apiLogAttribute.OperationEnum = OperationEnum.Edit;
  118. if (id > 0)
  119. {
  120. updatePreData = await TableInfoToJson(sqlSugar, apiLogAttribute.TableName, id);
  121. }
  122. }
  123. }
  124. // 根据id判断操作类型
  125. else if (id > 0 && apiLogAttribute.OperationEnum != OperationEnum.Del)
  126. {
  127. apiLogAttribute.OperationEnum = OperationEnum.Edit;
  128. updatePreData = await TableInfoToJson(sqlSugar, apiLogAttribute.TableName, id);
  129. }
  130. else if (apiLogAttribute.OperationEnum != OperationEnum.Del && id < 1)
  131. {
  132. apiLogAttribute.OperationEnum = OperationEnum.Add;
  133. }
  134. }
  135. }
  136. }
  137. catch (JsonException)
  138. {
  139. _logger.LogDebug("JSON解析失败,可能请求体不是JSON格式");
  140. }
  141. catch (Exception ex)
  142. {
  143. _logger.LogError(ex, "解析请求参数时发生错误");
  144. }
  145. }
  146. // 保存原始响应体流
  147. var originalResponseBody = context.Response.Body;
  148. // 创建一个新的内存流来捕获响应体
  149. using var responseMemoryStream = new MemoryStream();
  150. context.Response.Body = responseMemoryStream;
  151. // 调用下一个中间件
  152. await _next(context);
  153. // 重置响应体流的位置
  154. responseMemoryStream.Position = 0;
  155. // 读取响应体内容
  156. var responseBodyText = await ReadResponseBody(responseMemoryStream);
  157. // 将响应体内容写回原始响应体流
  158. await responseMemoryStream.CopyToAsync(originalResponseBody);
  159. // 修改后数据查询
  160. if (status == 2 && id > 0)
  161. {
  162. using (var scope = _serviceProvider.CreateScope())
  163. {
  164. var sqlSugar = scope.ServiceProvider.GetRequiredService<SqlSugarClient>();
  165. updateBefData = await TableInfoToJson(sqlSugar, apiLogAttribute.TableName, id);
  166. }
  167. }
  168. else if (apiLogAttribute.OperationEnum == OperationEnum.Edit && id > 0)
  169. {
  170. using (var scope = _serviceProvider.CreateScope())
  171. {
  172. var sqlSugar = scope.ServiceProvider.GetRequiredService<SqlSugarClient>();
  173. updateBefData = await TableInfoToJson(sqlSugar, apiLogAttribute.TableName, id);
  174. }
  175. }
  176. // 获取IP信息和设备信息
  177. string remoteIp = GetClientIp(context);
  178. string location = await GetIpLocationSafe(remoteIp);
  179. var (deviceType, browser, os) = GetDeviceInfo(context);
  180. // 记录请求结束时间
  181. var endTime = DateTime.UtcNow;
  182. // 计算耗时
  183. var duration = (long)(endTime - startTime).TotalMilliseconds;
  184. // 截断过长的文本
  185. var truncatedRequestBody = TruncateString(requestBodyText, 4000);
  186. var truncatedResponseBody = TruncateString(responseBodyText, 4000);
  187. var truncatedPreData = TruncateString(updatePreData, 4000);
  188. var truncatedBefData = TruncateString(updateBefData, 4000);
  189. var logInfo = new Crm_TableOperationRecord()
  190. {
  191. TableName = apiLogAttribute.TableName,
  192. PortType = portType,
  193. OperationItem = apiLogAttribute.OperationEnum,
  194. DataId = id,
  195. RequestUrl = context.Request.Path + context.Request.QueryString,
  196. RemoteIp = remoteIp,
  197. Location = location,
  198. RequestParam = !string.IsNullOrEmpty(truncatedRequestBody) ? JsonConvert.SerializeObject(truncatedRequestBody) : null,
  199. ReturnResult = !string.IsNullOrEmpty(truncatedResponseBody) ? JsonConvert.SerializeObject(truncatedResponseBody) : null,
  200. Elapsed = duration,
  201. Status = context.Response.StatusCode.ToString(),
  202. CreateUserId = userId,
  203. UpdatePreData = truncatedPreData,
  204. UpdateBefData = truncatedBefData,
  205. Browser = browser,
  206. Os = os,
  207. DeviceType = deviceType,
  208. CreateTime = DateTime.Now
  209. };
  210. // 异步记录日志,不阻塞响应
  211. _ = Task.Run(async () =>
  212. {
  213. try
  214. {
  215. // 在异步任务中创建新的作用域
  216. using var logScope = _serviceProvider.CreateScope();
  217. var logSqlSugar = logScope.ServiceProvider.GetRequiredService<SqlSugarClient>();
  218. // 设置较短的超时时间
  219. logSqlSugar.Ado.CommandTimeOut = 3;
  220. // 存储到数据库
  221. await logSqlSugar.Insertable(logInfo).ExecuteCommandAsync();
  222. }
  223. catch (Exception ex)
  224. {
  225. _logger.LogError(ex, "异步记录操作日志失败");
  226. // 降级:写入文件日志
  227. try
  228. {
  229. await WriteLogToFile(apiLogAttribute, requestBodyText, responseBodyText, userId, ex.Message);
  230. }
  231. catch (Exception fileEx)
  232. {
  233. _logger.LogError(fileEx, "写入文件日志失败");
  234. }
  235. }
  236. });
  237. }
  238. else
  239. {
  240. await _next(context);
  241. }
  242. }
  243. /// <summary>
  244. /// 从请求参数中解析用户ID
  245. /// </summary>
  246. private int ParseUserIdFromParams(Dictionary<string, object> requestBodyJson)
  247. {
  248. var userIdKeys = new[] { "userId", "currUserId", "createUserId", "operationUserId", "deleteUserId" };
  249. foreach (var key in userIdKeys)
  250. {
  251. if (requestBodyJson.TryGetValue(key, out var value) && value != null)
  252. {
  253. if (int.TryParse(value.ToString(), out var parsedUserId) && parsedUserId > 0)
  254. {
  255. return parsedUserId;
  256. }
  257. }
  258. }
  259. return 0;
  260. }
  261. /// <summary>
  262. /// 获取表数据JSON
  263. /// </summary>
  264. private async Task<string> TableInfoToJson(SqlSugarClient sqlSugar, string tableName, int id)
  265. {
  266. if (sqlSugar.DbMaintenance.IsAnyTable(tableName))
  267. {
  268. try
  269. {
  270. string jsonLabel = string.Empty;
  271. if (tableName.Equals("Crm_NewClientData"))
  272. {
  273. var info = await sqlSugar.Queryable<Crm_NewClientData>()
  274. .Where(x => x.Id == id)
  275. .FirstAsync();
  276. if (info != null)
  277. {
  278. EncryptionProcessor.DecryptProperties(info);
  279. if (info != null)
  280. jsonLabel = JsonConvert.SerializeObject(info);
  281. }
  282. }
  283. else
  284. {
  285. var sql = $"SELECT * FROM {tableName} WHERE Id = {id}";
  286. var info = await sqlSugar.SqlQueryable<dynamic>(sql).FirstAsync();
  287. if (info != null)
  288. jsonLabel = JsonConvert.SerializeObject(info);
  289. }
  290. return jsonLabel;
  291. }
  292. catch (Exception ex)
  293. {
  294. _logger.LogError(ex, $"获取表数据失败: {tableName}, Id: {id}");
  295. return string.Empty;
  296. }
  297. }
  298. return string.Empty;
  299. }
  300. /// <summary>
  301. /// 是否是静态文件请求
  302. /// </summary>
  303. private bool IsStaticFileRequest(PathString path)
  304. {
  305. try
  306. {
  307. var staticExtensions = new[] {
  308. ".css", ".js", ".png", ".jpg", ".jpeg", ".gif",
  309. ".ico", ".svg", ".woff", ".woff2", ".ttf", ".eot",
  310. ".mp4", ".mp3", ".avi", ".mov", ".wmv", ".flv",
  311. ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
  312. ".zip", ".rar", ".7z", ".tar", ".gz"
  313. };
  314. var pathString = path.Value?.ToLower() ?? string.Empty;
  315. return staticExtensions.Any(ext => pathString.EndsWith(ext.ToLower(), StringComparison.OrdinalIgnoreCase));
  316. }
  317. catch
  318. {
  319. return false;
  320. }
  321. }
  322. /// <summary>
  323. /// 读取请求体
  324. /// </summary>
  325. private async Task<string> ReadRequestBody(HttpRequest request)
  326. {
  327. try
  328. {
  329. request.Body.Position = 0;
  330. using var reader = new StreamReader(
  331. request.Body,
  332. Encoding.UTF8,
  333. detectEncodingFromByteOrderMarks: false,
  334. bufferSize: 8192,
  335. leaveOpen: true
  336. );
  337. var body = await reader.ReadToEndAsync();
  338. request.Body.Position = 0;
  339. return body;
  340. }
  341. catch (Exception ex)
  342. {
  343. _logger.LogError(ex, "读取请求体失败");
  344. return string.Empty;
  345. }
  346. }
  347. /// <summary>
  348. /// 读取响应体
  349. /// </summary>
  350. private async Task<string> ReadResponseBody(Stream stream)
  351. {
  352. try
  353. {
  354. using var reader = new StreamReader(
  355. stream,
  356. Encoding.UTF8,
  357. detectEncodingFromByteOrderMarks: false,
  358. bufferSize: 8192,
  359. leaveOpen: true
  360. );
  361. var body = await reader.ReadToEndAsync();
  362. stream.Position = 0;
  363. return body;
  364. }
  365. catch (Exception ex)
  366. {
  367. _logger.LogError(ex, "读取响应体失败");
  368. return string.Empty;
  369. }
  370. }
  371. /// <summary>
  372. /// 读取Token
  373. /// </summary>
  374. private async Task<int> ReadToken(HttpContext context)
  375. {
  376. try
  377. {
  378. var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
  379. if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
  380. {
  381. var authInfo = JwtHelper.SerializeJwt(authHeader);
  382. if (authInfo != null)
  383. return authInfo.UserId;
  384. }
  385. }
  386. catch (Exception ex)
  387. {
  388. _logger.LogError(ex, "读取Token失败");
  389. }
  390. return 0;
  391. }
  392. /// <summary>
  393. /// 获取客户端IP
  394. /// </summary>
  395. private string GetClientIp(HttpContext context)
  396. {
  397. try
  398. {
  399. // 优先从X-Forwarded-For获取
  400. if (context.Request.Headers.ContainsKey("X-Forwarded-For"))
  401. {
  402. var xForwardedFor = context.Request.Headers["X-Forwarded-For"].ToString();
  403. if (!string.IsNullOrEmpty(xForwardedFor))
  404. {
  405. var ips = xForwardedFor.Split(',', StringSplitOptions.RemoveEmptyEntries);
  406. if (ips.Length > 0)
  407. return ips[0].Trim();
  408. }
  409. }
  410. // 其次从X-Real-IP获取
  411. if (context.Request.Headers.ContainsKey("X-Real-IP"))
  412. {
  413. var xRealIp = context.Request.Headers["X-Real-IP"].ToString();
  414. if (!string.IsNullOrEmpty(xRealIp))
  415. return xRealIp.Trim();
  416. }
  417. // 最后从RemoteIpAddress获取
  418. var remoteIp = context.Connection.RemoteIpAddress?.ToString();
  419. if (!string.IsNullOrEmpty(remoteIp))
  420. {
  421. // 处理IPv6映射的IPv4地址
  422. if (remoteIp.StartsWith("::ffff:"))
  423. return remoteIp.Substring(7);
  424. return remoteIp;
  425. }
  426. return "Unknown";
  427. }
  428. catch
  429. {
  430. return "Unknown";
  431. }
  432. }
  433. /// <summary>
  434. /// 安全获取IP位置
  435. /// </summary>
  436. private async Task<string> GetIpLocationSafe(string ip)
  437. {
  438. if (string.IsNullOrWhiteSpace(ip) || ip == "Unknown" || ip == "::1" || ip == "127.0.0.1")
  439. return "本地";
  440. try
  441. {
  442. // IPv6地址
  443. if (ip.Contains(":"))
  444. return "IPv6";
  445. // 检查是否是内网地址
  446. if (IsPrivateIp(ip))
  447. return "内网";
  448. // 可以在这里调用IP查询API
  449. // 但为了性能,暂时返回未知
  450. return "未知";
  451. }
  452. catch (Exception ex)
  453. {
  454. _logger.LogWarning(ex, $"获取IP位置失败: {ip}");
  455. return "未知";
  456. }
  457. }
  458. /// <summary>
  459. /// 检查是否是内网IP
  460. /// </summary>
  461. private bool IsPrivateIp(string ip)
  462. {
  463. try
  464. {
  465. if (string.IsNullOrWhiteSpace(ip))
  466. return false;
  467. // 常见的私有IP地址段
  468. if (ip.StartsWith("10.") ||
  469. ip.StartsWith("192.168.") ||
  470. ip.StartsWith("172.16.") ||
  471. ip.StartsWith("172.17.") ||
  472. ip.StartsWith("172.18.") ||
  473. ip.StartsWith("172.19.") ||
  474. ip.StartsWith("172.20.") ||
  475. ip.StartsWith("172.21.") ||
  476. ip.StartsWith("172.22.") ||
  477. ip.StartsWith("172.23.") ||
  478. ip.StartsWith("172.24.") ||
  479. ip.StartsWith("172.25.") ||
  480. ip.StartsWith("172.26.") ||
  481. ip.StartsWith("172.27.") ||
  482. ip.StartsWith("172.28.") ||
  483. ip.StartsWith("172.29.") ||
  484. ip.StartsWith("172.30.") ||
  485. ip.StartsWith("172.31."))
  486. {
  487. return true;
  488. }
  489. return false;
  490. }
  491. catch
  492. {
  493. return false;
  494. }
  495. }
  496. /// <summary>
  497. /// 获取设备信息
  498. /// </summary>
  499. private (string deviceType, string browser, string os) GetDeviceInfo(HttpContext context)
  500. {
  501. try
  502. {
  503. var userAgent = context.Request.Headers["User-Agent"].FirstOrDefault();
  504. if (string.IsNullOrEmpty(userAgent))
  505. return ("Unknown", "Unknown", "Unknown");
  506. var parser = Parser.GetDefault();
  507. var client = parser.Parse(userAgent);
  508. // 提取浏览器信息
  509. var browser = client.UA.Family;
  510. var browserVersion = new List<string>();
  511. if (!string.IsNullOrEmpty(client.UA.Major))
  512. browserVersion.Add(client.UA.Major);
  513. if (!string.IsNullOrEmpty(client.UA.Minor))
  514. browserVersion.Add(client.UA.Minor);
  515. if (!string.IsNullOrEmpty(client.UA.Patch))
  516. browserVersion.Add(client.UA.Patch);
  517. if (browserVersion.Any())
  518. browser += " " + string.Join(".", browserVersion);
  519. // 提取操作系统信息
  520. var os = client.OS.Family;
  521. var osVersion = new List<string>();
  522. if (!string.IsNullOrEmpty(client.OS.Major))
  523. osVersion.Add(client.OS.Major);
  524. if (!string.IsNullOrEmpty(client.OS.Minor))
  525. osVersion.Add(client.OS.Minor);
  526. if (!string.IsNullOrEmpty(client.OS.Patch))
  527. osVersion.Add(client.OS.Patch);
  528. if (!string.IsNullOrEmpty(client.OS.PatchMinor))
  529. osVersion.Add(client.OS.PatchMinor);
  530. if (osVersion.Any())
  531. os += " " + string.Join(".", osVersion);
  532. // 提取设备信息
  533. var deviceType = client.Device.Family;
  534. if (string.Equals(deviceType, "Other", StringComparison.OrdinalIgnoreCase))
  535. {
  536. // 根据userAgent判断设备类型
  537. var ua = userAgent.ToLower();
  538. if (ua.Contains("mobile") || ua.Contains("android") || ua.Contains("iphone") || ua.Contains("ipad"))
  539. deviceType = "Mobile";
  540. else
  541. deviceType = "Desktop";
  542. }
  543. return (deviceType, browser, os);
  544. }
  545. catch (Exception ex)
  546. {
  547. _logger.LogDebug(ex, "解析设备信息失败");
  548. return ("Unknown", "Unknown", "Unknown");
  549. }
  550. }
  551. /// <summary>
  552. /// 截断字符串
  553. /// </summary>
  554. private string TruncateString(string input, int maxLength)
  555. {
  556. if (string.IsNullOrEmpty(input) || input.Length <= maxLength)
  557. return input;
  558. return input.Substring(0, maxLength) + "...[已截断]";
  559. }
  560. /// <summary>
  561. /// 将日志写入文件
  562. /// </summary>
  563. private async Task WriteLogToFile(ApiLogAttribute apiLogAttribute, string requestBody, string responseBody, int userId, string error)
  564. {
  565. try
  566. {
  567. var logDir = Path.Combine(Directory.GetCurrentDirectory(), "Logs", "OperationLogs");
  568. Directory.CreateDirectory(logDir);
  569. var logFile = Path.Combine(logDir, $"operation_fallback_{DateTime.Now:yyyyMMdd}.log");
  570. var logEntry = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} | " +
  571. $"Table: {apiLogAttribute.TableName} | " +
  572. $"Operation: {apiLogAttribute.OperationEnum} | " +
  573. $"User: {userId} | " +
  574. $"Request: {TruncateString(requestBody, 500)} | " +
  575. $"Response: {TruncateString(responseBody, 500)} | " +
  576. $"Error: {error}" +
  577. Environment.NewLine;
  578. await File.AppendAllTextAsync(logFile, logEntry);
  579. }
  580. catch (Exception ex)
  581. {
  582. _logger.LogError(ex, "写入文件日志失败");
  583. }
  584. }
  585. }
  586. }