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 "";