diff --git a/Http/Attributes/RateLimitAtributte.cs b/Http/Attributes/RateLimitAttribute.cs similarity index 67% rename from Http/Attributes/RateLimitAtributte.cs rename to Http/Attributes/RateLimitAttribute.cs index 0097bed..1486320 100644 --- a/Http/Attributes/RateLimitAtributte.cs +++ b/Http/Attributes/RateLimitAttribute.cs @@ -1,30 +1,57 @@ -using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; using System.Collections.Concurrent; using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; namespace Http.Attributes; [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] +/// +/// Atributo que limita la cantidad de solicitudes permitidas por usuario. +/// public class RateLimitAttribute(int requestLimit = 20, int timeWindowSeconds = 60, int blockDurationSeconds = 60) : ActionFilterAttribute { - private static ConcurrentDictionary _requests = new(); + /// + /// Registro de solicitudes por identificador. + /// + private static readonly ConcurrentDictionary _requests = new(); + + /// + /// Límite máximo de solicitudes permitidas. + /// private readonly int _requestLimit = requestLimit; + + /// + /// Intervalo de tiempo para contar solicitudes. + /// private readonly TimeSpan _timeWindow = TimeSpan.FromSeconds(timeWindowSeconds); + + /// + /// Duración del bloqueo cuando se excede el límite. + /// private readonly TimeSpan _blockDuration = TimeSpan.FromSeconds(blockDurationSeconds); - private readonly string key = Guid.NewGuid().ToString(); + /// + /// Clave única para diferenciar instancias. + /// + private readonly string _key = Guid.NewGuid().ToString(); + + /// + /// Valida la frecuencia de solicitudes antes de ejecutar la acción. + /// public override void OnActionExecuting(ActionExecutingContext context) { - + // Obtener el identificador primario del usuario a partir del token. var userId = GetPrimaryId(context.HttpContext.Request.Headers["token"].FirstOrDefault()); var now = DateTime.UtcNow; - string cache = $"{key}-{userId}"; + var cacheKey = $"{_key}-{userId}"; - if (_requests.TryGetValue(cache, out RequestInfo? requestInfo)) + if (_requests.TryGetValue(cacheKey, out RequestInfo? requestInfo)) { - // Verificar si está bloqueado + // Verificar si el usuario está bloqueado. if (requestInfo.BlockedUntil.HasValue && requestInfo.BlockedUntil > now) { // Respuesta. @@ -33,7 +60,7 @@ public override void OnActionExecuting(ActionExecutingContext context) Response = Responses.RateLimitExceeded, Message = $"Has excedido el límite de solicitudes. Intenta nuevamente después de {requestInfo.BlockedUntil - now:hh\\:mm\\:ss}." }; - // Bloquear la solicitud con un código 429 + // Bloquear la solicitud con un código 429. context.Result = new ContentResult { Content = System.Text.Json.JsonSerializer.Serialize(response), @@ -44,11 +71,11 @@ public override void OnActionExecuting(ActionExecutingContext context) } else if (requestInfo.BlockedUntil.HasValue && requestInfo.BlockedUntil <= now) { - // Si el tiempo de bloqueo ha pasado, reiniciar el contador + // Si el tiempo de bloqueo ha pasado, reiniciar el contador. requestInfo.Reset(now); } - // Verificar si está dentro del intervalo de tiempo + // Verificar si está dentro del intervalo de tiempo. if (requestInfo.LastRequestTime.Add(_timeWindow) > now) { requestInfo.RequestCount++; @@ -61,7 +88,7 @@ public override void OnActionExecuting(ActionExecutingContext context) Response = Responses.RateLimitExceeded, Message = $"Has excedido el límite de solicitudes. Intenta nuevamente después de {requestInfo.BlockedUntil - now:hh\\:mm\\:ss}." }; - // Bloquear al usuario + // Bloquear al usuario. requestInfo.BlockedUntil = now.Add(_blockDuration); context.Result = new ContentResult { @@ -74,21 +101,21 @@ public override void OnActionExecuting(ActionExecutingContext context) } else { - // Reiniciar el contador si ha pasado el tiempo + // Reiniciar el contador si ha pasado el tiempo. requestInfo.Reset(now); } } else { - // Primera solicitud del usuario - _requests.TryAdd(cache, new RequestInfo(now)); + // Primera solicitud del usuario. + _requests.TryAdd(cacheKey, new RequestInfo(now)); } base.OnActionExecuting(context); } /// - /// Obtener el primary id del token JWT. + /// Obtiene el identificador primario del token JWT. /// /// Token a validar. private static string GetPrimaryId(string? token) @@ -99,18 +126,19 @@ private static string GetPrimaryId(string? token) try { - // Decodificar el JWT sin verificar la firma + // Decodificar el JWT sin verificar la firma. var handler = new JwtSecurityTokenHandler(); var jsonToken = handler.ReadJwtToken(token); var payload = jsonToken.Payload; - // Obtener el primary id del payload - var primarySid = payload.FirstOrDefault(p => p.Key == "http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid").Value; + // Obtener el identificador principal del payload. + var primarySid = payload.FirstOrDefault(p => p.Key == ClaimTypes.PrimarySid).Value; return primarySid?.ToString() ?? ""; } catch (Exception) { + // Ignorar errores de decodificación. } return ""; @@ -119,8 +147,19 @@ private static string GetPrimaryId(string? token) internal class RequestInfo { + /// + /// Cantidad de solicitudes realizadas. + /// public int RequestCount { get; set; } + + /// + /// Momento de la última solicitud. + /// public DateTime LastRequestTime { get; set; } + + /// + /// Tiempo hasta el cual el usuario está bloqueado. + /// public DateTime? BlockedUntil { get; set; } public RequestInfo(DateTime requestTime) diff --git a/Http/Extensions/HttpExtensions.cs b/Http/Extensions/HttpExtensions.cs index 405261a..3f56eb4 100644 --- a/Http/Extensions/HttpExtensions.cs +++ b/Http/Extensions/HttpExtensions.cs @@ -1,4 +1,4 @@ -using Http.Middlewares; +using Http.Middlewares; using LIN.Access.Logger; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; @@ -56,13 +56,13 @@ public static IServiceCollection AddLINHttp(this IServiceCollection services, bo /// - /// Usar rate token limit. + /// Habilita el middleware de limitación de solicitudes por token. /// - /// Limite. - /// Tiempo. + /// Límite máximo de solicitudes. + /// Duración del intervalo de evaluación. public static IApplicationBuilder UseRateTokenLimit(this IApplicationBuilder app, int limit, TimeSpan time) { - RateTokenLimitingMiddleware.TimeSpan = time; + RateTokenLimitingMiddleware.TimeWindow = time; RateTokenLimitingMiddleware.RequestLimit = limit; app.UseMiddleware(); app.UseMiddleware(); diff --git a/Http/Middlewares/RateTokenLimitingMiddleware.cs b/Http/Middlewares/RateTokenLimitingMiddleware.cs index 2f616fb..9dccadb 100644 --- a/Http/Middlewares/RateTokenLimitingMiddleware.cs +++ b/Http/Middlewares/RateTokenLimitingMiddleware.cs @@ -1,45 +1,49 @@ -using System.IdentityModel.Tokens.Jwt; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; namespace Http.Middlewares; +/// +/// Middleware que limita la cantidad de solicitudes por usuario mediante un token JWT. +/// public class RateTokenLimitingMiddleware(RequestDelegate next) { /// - /// Cache. + /// Registro de solicitudes realizadas por cada usuario. /// private static readonly Dictionary _userRequestLog = new(); /// - /// Limite. + /// Límite máximo de solicitudes permitidas por intervalo. /// internal static int RequestLimit { get; set; } /// - /// Tiempo de bloqueo. + /// Duración del intervalo en el cual se contabilizan las solicitudes. /// - internal static TimeSpan TimeSpan { get; set; } + internal static TimeSpan TimeWindow { get; set; } /// - /// AL invocar. + /// Procesa la solicitud y aplica el límite de solicitudes configurado. /// public async Task InvokeAsync(HttpContext context) { - // Aquí obtienes el identificador del usuario (por ejemplo, un nombre de usuario o ID único) + // Obtener el identificador primario del usuario a partir del token. var userId = GetPrimaryId(context.Request.Headers["token"].FirstOrDefault()); - // Si existe el usuario. + // Si se identificó un usuario. if (!string.IsNullOrEmpty(userId)) { if (_userRequestLog.ContainsKey(userId)) { var (timestamp, requestCount) = _userRequestLog[userId]; - // Verificar si el tiempo actual está dentro del mismo intervalo de tiempo (1 minuto) - if (DateTime.UtcNow - timestamp < TimeSpan) + // Verificar si la solicitud se encuentra dentro del intervalo configurado. + if (DateTime.UtcNow - timestamp < TimeWindow) { if (requestCount >= RequestLimit) { @@ -56,31 +60,31 @@ public async Task InvokeAsync(HttpContext context) } else { - // Aumentar el contador de solicitudes + // Aumentar el contador de solicitudes. _userRequestLog[userId] = (timestamp, requestCount + 1); } } else { - // Reiniciar el contador y el timestamp si el intervalo de tiempo ha pasado + // Reiniciar el contador y el timestamp si el intervalo de tiempo ha pasado. _userRequestLog[userId] = (DateTime.UtcNow, 1); } } else { - // Agregar una nueva entrada para un usuario nuevo o sin registros previos + // Agregar una nueva entrada para un usuario nuevo o sin registros previos. _userRequestLog[userId] = (DateTime.UtcNow, 1); } } - // Pasar la solicitud al siguiente middleware si no se excede el límite + // Pasar la solicitud al siguiente middleware si no se excede el límite. await next(context); } /// - /// Obtener el primary id del token JWT. + /// Obtiene el identificador primario del token JWT. /// /// Token a validar. private static string GetPrimaryId(string? token) @@ -91,18 +95,19 @@ private static string GetPrimaryId(string? token) try { - // Decodificar el JWT sin verificar la firma + // Decodificar el JWT sin verificar la firma. var handler = new JwtSecurityTokenHandler(); var jsonToken = handler.ReadJwtToken(token); var payload = jsonToken.Payload; - // Obtener el primary id del payload - var primarySid = payload.FirstOrDefault(p => p.Key == "http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid").Value; + // Obtener el identificador principal del payload. + var primarySid = payload.FirstOrDefault(p => p.Key == ClaimTypes.PrimarySid).Value; return primarySid?.ToString() ?? ""; } catch (Exception) { + // Ignorar errores de decodificación. } return "";