AdminAccessLogMiddleware.cs 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. using SharedKernel.Constants;
  2. using Application.Abstractions.Data;
  3. using Domain.Entities.Director;
  4. using System.Diagnostics;
  5. namespace Admin.Middlewares;
  6. public class AdminAccessLogMiddleware(RequestDelegate next)
  7. {
  8. // 로깅 제외 경로
  9. private static readonly string[] ExcludePrefixes =
  10. [
  11. "/lib/",
  12. "/css/",
  13. "/js/",
  14. "/images/",
  15. "/favicon",
  16. "/_framework",
  17. "/_blazor",
  18. "/.well-known/",
  19. "/Identity/Account"
  20. ];
  21. // 로깅 제외 확장자
  22. private static readonly string[] ExcludeExtensions =
  23. [
  24. ".css",
  25. ".js",
  26. ".map",
  27. ".png",
  28. ".jpg",
  29. ".jpeg",
  30. ".gif",
  31. ".svg",
  32. ".ico",
  33. ".woff",
  34. ".woff2",
  35. ".ttf",
  36. ".eot"
  37. ];
  38. public async Task InvokeAsync(HttpContext context)
  39. {
  40. var path = context.Request.Path.Value ?? "";
  41. // 정적 파일, Identity 페이지 제외
  42. if (ShouldSkip(path))
  43. {
  44. await next(context);
  45. return;
  46. }
  47. // 인증되지 않은 사용자 제외
  48. if (context.User.Identity is not { IsAuthenticated: true })
  49. {
  50. await next(context);
  51. return;
  52. }
  53. var sw = Stopwatch.StartNew(); // 시작 시간
  54. await next(context);
  55. sw.Stop(); // 종료 시간
  56. try
  57. {
  58. var db = context.RequestServices.GetRequiredService<IAppDbContext>();
  59. var userID = context.User.Identity.Name ?? "-";
  60. var userName = context.User.FindFirst(System.Security.Claims.ClaimTypes.GivenName)?.Value;
  61. var method = context.Request.Method;
  62. var queryString = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : null;
  63. var statusCode = context.Response.StatusCode;
  64. var elapsedMs = sw.ElapsedMilliseconds;
  65. var menuName = ResolveMenuName(path);
  66. var ipAddress = context.Connection.RemoteIpAddress?.ToString();
  67. var userAgent = context.Request.Headers.UserAgent.ToString();
  68. var log = AdminAccessLog.Create(
  69. userID,
  70. userName,
  71. method,
  72. path,
  73. queryString,
  74. statusCode,
  75. elapsedMs,
  76. menuName,
  77. ipAddress,
  78. userAgent);
  79. await db.AdminAccessLog.AddAsync(log);
  80. await db.SaveChangesAsync();
  81. }
  82. catch
  83. {
  84. }
  85. }
  86. // 로깅 제외 경로 및 확장자 확인
  87. private static bool ShouldSkip(string path)
  88. {
  89. foreach (var prefix in ExcludePrefixes)
  90. {
  91. if (path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
  92. {
  93. return true;
  94. }
  95. }
  96. foreach (var ext in ExcludeExtensions)
  97. {
  98. if (path.EndsWith(ext, StringComparison.OrdinalIgnoreCase))
  99. {
  100. return true;
  101. }
  102. }
  103. return false;
  104. }
  105. // 경로에 해당하는 메뉴 이름 찾기
  106. private static string? ResolveMenuName(string path)
  107. {
  108. var menus = Menus.GetMenus();
  109. return FindMenuName(menus, path.TrimEnd('/'));
  110. }
  111. // 재귀적으로 메뉴 트리 탐색
  112. private static string? FindMenuName(List<Menu> menus, string path)
  113. {
  114. foreach (var menu in menus)
  115. {
  116. if (!string.IsNullOrEmpty(menu.Path))
  117. {
  118. var menuPath = menu.Path.TrimEnd('/');
  119. if (path.Equals(menuPath, StringComparison.OrdinalIgnoreCase) || path.StartsWith(menuPath + "/", StringComparison.OrdinalIgnoreCase))
  120. {
  121. return menu.Name;
  122. }
  123. }
  124. if (menu.Children is { Count: > 0 })
  125. {
  126. var childResult = FindMenuName(menu.Children, path);
  127. if (childResult is not null)
  128. {
  129. return childResult;
  130. }
  131. }
  132. }
  133. return null;
  134. }
  135. }