| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869 |
- using System.Net;
- using System.Security.Claims;
- using Application.Abstractions.RateLimit;
- namespace Web.Api.Middleware;
- /// <summary>
- /// /v1/* 공개 API endpoint 의 rate limit 미들웨어.
- /// 기본 60 req/min per token. 토큰이 없으면 IP 단위.
- /// </summary>
- public sealed class RateLimitMiddleware(RequestDelegate next, IRateLimiter limiter)
- {
- private const int DefaultLimitPerMinute = 60;
- private static readonly TimeSpan Window = TimeSpan.FromMinutes(1);
- public async Task InvokeAsync(HttpContext context)
- {
- if (!context.Request.Path.StartsWithSegments("/v1"))
- {
- await next(context);
- return;
- }
- var key = ResolveKey(context);
- var result = await limiter.CheckAsync(key, DefaultLimitPerMinute, Window, context.RequestAborted);
- context.Response.Headers["X-RateLimit-Limit"] = result.Limit.ToString();
- context.Response.Headers["X-RateLimit-Remaining"] = result.Remaining.ToString();
- context.Response.Headers["X-RateLimit-Reset"] = new DateTimeOffset(result.ResetAt, TimeSpan.Zero).ToUnixTimeSeconds().ToString();
- if (!result.Allowed)
- {
- context.Response.StatusCode = (int)HttpStatusCode.TooManyRequests;
- context.Response.ContentType = "application/problem+json";
- await context.Response.WriteAsJsonAsync(new
- {
- type = "https://tools.ietf.org/html/rfc6585#section-4",
- title = "Too Many Requests",
- status = 429,
- detail = "Rate limit exceeded. Try again later."
- }, context.RequestAborted);
- return;
- }
- await next(context);
- }
- private static string ResolveKey(HttpContext context)
- {
- if (context.User.Identity?.IsAuthenticated == true)
- {
- var tokenID = context.User.FindFirst("token_id")?.Value;
- if (!string.IsNullOrEmpty(tokenID))
- {
- return $"tok:{tokenID}";
- }
- var sub = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value
- ?? context.User.FindFirst("sub")?.Value;
- if (!string.IsNullOrEmpty(sub))
- {
- return $"sub:{sub}";
- }
- }
- var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
- return $"ip:{ip}";
- }
- }
|