HttpContextExtension.cs 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. using Microsoft.AspNetCore.Http;
  2. using System.Security.Claims;
  3. namespace SharedKernel.Extensions;
  4. public static class HttpContextExtension
  5. {
  6. // 회원 ID 조회
  7. public static int GetMemberID(this HttpContext context)
  8. {
  9. return int.TryParse(context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value, out var id) ? id : 0;
  10. }
  11. // 회원 이름 조회
  12. public static string? UserName(this HttpContext context)
  13. {
  14. return context.User.Identity?.Name;
  15. }
  16. // 이전 요청 URL 조회
  17. public static string? GetReferer(this HttpContext context)
  18. {
  19. if (context.Request.Headers.TryGetValue("Referer", out var referer) && !string.IsNullOrWhiteSpace(referer))
  20. {
  21. return referer.ToString();
  22. }
  23. return null;
  24. }
  25. // Client IP 조회 — Cloudflare → IIS → backend reverse proxy 환경 대응
  26. // 우선순위:
  27. // 0) X-Original-Client-IP (Next.js proxy 가 Cloudflare 더블 경유 우회용으로 forward 한 진짜 user IP)
  28. // 1) CF-Connecting-IP (Cloudflare 가 보장하는 client IP, 단 더블 경유 시 Next.js server IP)
  29. // 2) X-Real-IP (일부 proxy 가 채움)
  30. // 3) X-Forwarded-For (comma-separated 의 가장 왼쪽 = 원래 client, 첫 값만)
  31. // 4) RemoteIpAddress (ForwardedHeaders middleware 가 채워주는 경우)
  32. // IPv4-mapped IPv6 (`::ffff:127.0.0.1`) → `127.0.0.1` 로 정규화. IP 가 아닌 값은 45 char 로 방어 제한.
  33. public static string? GetClientIP(this HttpContext context)
  34. {
  35. if (context.Request.Headers.TryGetValue("X-Original-Client-IP", out var orig) && !string.IsNullOrWhiteSpace(orig))
  36. {
  37. return NormalizeIP(orig.ToString().Trim());
  38. }
  39. if (context.Request.Headers.TryGetValue("CF-Connecting-IP", out var cf) && !string.IsNullOrWhiteSpace(cf))
  40. {
  41. return NormalizeIP(cf.ToString().Trim());
  42. }
  43. if (context.Request.Headers.TryGetValue("X-Real-IP", out var xri) && !string.IsNullOrWhiteSpace(xri))
  44. {
  45. return NormalizeIP(xri.ToString().Trim());
  46. }
  47. var forwardedFor = context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
  48. if (!string.IsNullOrWhiteSpace(forwardedFor))
  49. {
  50. // comma-separated 의 가장 왼쪽 = 원래 client (RFC 7239 / standard)
  51. var first = forwardedFor.Split(',', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
  52. if (!string.IsNullOrWhiteSpace(first))
  53. {
  54. return NormalizeIP(first);
  55. }
  56. }
  57. return NormalizeIP(context.Connection.RemoteIpAddress?.ToString());
  58. }
  59. private static string? NormalizeIP(string? ip)
  60. {
  61. if (string.IsNullOrWhiteSpace(ip))
  62. {
  63. return null;
  64. }
  65. // IPv4-mapped IPv6 (e.g. "::ffff:127.0.0.1") → IPv4 만 추출
  66. if (System.Net.IPAddress.TryParse(ip, out var parsed))
  67. {
  68. if (parsed.IsIPv4MappedToIPv6)
  69. {
  70. return parsed.MapToIPv4().ToString();
  71. }
  72. return parsed.ToString();
  73. }
  74. // IP 형식이 아니면 컬럼 최대 길이(45)로 방어적 제한
  75. return ip.Length > 45 ? ip[..45] : ip;
  76. }
  77. // User-agent 조회 — Next.js Route Handler 가 forward 한 X-Original-User-Agent 최우선.
  78. public static string? GetUserAgent(this HttpContext context)
  79. {
  80. if (context.Request.Headers.TryGetValue("X-Original-User-Agent", out var origUa) && !string.IsNullOrWhiteSpace(origUa))
  81. {
  82. return origUa.ToString();
  83. }
  84. if (context.Request.Headers.TryGetValue("User-Agent", out var userAgent) && !string.IsNullOrWhiteSpace(userAgent))
  85. {
  86. return userAgent.ToString();
  87. }
  88. return null;
  89. }
  90. // 상관관계 ID 조회
  91. public static string GetCorrelationID(this HttpContext context)
  92. {
  93. return context.TraceIdentifier;
  94. }
  95. }