Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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)]
/// <summary>
/// Atributo que limita la cantidad de solicitudes permitidas por usuario.
/// </summary>
public class RateLimitAttribute(int requestLimit = 20, int timeWindowSeconds = 60, int blockDurationSeconds = 60) : ActionFilterAttribute
{

private static ConcurrentDictionary<string, RequestInfo> _requests = new();
/// <summary>
/// Registro de solicitudes por identificador.
/// </summary>
private static readonly ConcurrentDictionary<string, RequestInfo> _requests = new();

/// <summary>
/// Límite máximo de solicitudes permitidas.
/// </summary>
private readonly int _requestLimit = requestLimit;

/// <summary>
/// Intervalo de tiempo para contar solicitudes.
/// </summary>
private readonly TimeSpan _timeWindow = TimeSpan.FromSeconds(timeWindowSeconds);

/// <summary>
/// Duración del bloqueo cuando se excede el límite.
/// </summary>
private readonly TimeSpan _blockDuration = TimeSpan.FromSeconds(blockDurationSeconds);
private readonly string key = Guid.NewGuid().ToString();

/// <summary>
/// Clave única para diferenciar instancias.
/// </summary>
private readonly string _key = Guid.NewGuid().ToString();

/// <summary>
/// Valida la frecuencia de solicitudes antes de ejecutar la acción.
/// </summary>
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.
Expand All @@ -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),
Expand All @@ -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++;
Expand All @@ -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
{
Expand All @@ -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);
}

/// <summary>
/// Obtener el primary id del token JWT.
/// Obtiene el identificador primario del token JWT.
/// </summary>
/// <param name="token">Token a validar.</param>
private static string GetPrimaryId(string? token)
Expand All @@ -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 "";

Expand All @@ -119,8 +147,19 @@ private static string GetPrimaryId(string? token)

internal class RequestInfo
{
/// <summary>
/// Cantidad de solicitudes realizadas.
/// </summary>
public int RequestCount { get; set; }

/// <summary>
/// Momento de la última solicitud.
/// </summary>
public DateTime LastRequestTime { get; set; }

/// <summary>
/// Tiempo hasta el cual el usuario está bloqueado.
/// </summary>
public DateTime? BlockedUntil { get; set; }

public RequestInfo(DateTime requestTime)
Expand Down
10 changes: 5 additions & 5 deletions Http/Extensions/HttpExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Http.Middlewares;
using Http.Middlewares;
using LIN.Access.Logger;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
Expand Down Expand Up @@ -56,13 +56,13 @@ public static IServiceCollection AddLINHttp(this IServiceCollection services, bo


/// <summary>
/// Usar rate token limit.
/// Habilita el middleware de limitación de solicitudes por token.
/// </summary>
/// <param name="limit">Limite.</param>
/// <param name="time">Tiempo.</param>
/// <param name="limit">Límite máximo de solicitudes.</param>
/// <param name="time">Duración del intervalo de evaluación.</param>
public static IApplicationBuilder UseRateTokenLimit(this IApplicationBuilder app, int limit, TimeSpan time)
{
RateTokenLimitingMiddleware.TimeSpan = time;
RateTokenLimitingMiddleware.TimeWindow = time;
RateTokenLimitingMiddleware.RequestLimit = limit;
app.UseMiddleware<RateTokenLimitingMiddleware>();
app.UseMiddleware<GatewayBasePathMiddleware>();
Expand Down
41 changes: 23 additions & 18 deletions Http/Middlewares/RateTokenLimitingMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,45 +1,49 @@
using System.IdentityModel.Tokens.Jwt;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;

namespace Http.Middlewares;

/// <summary>
/// Middleware que limita la cantidad de solicitudes por usuario mediante un token JWT.
/// </summary>
public class RateTokenLimitingMiddleware(RequestDelegate next)
{

/// <summary>
/// Cache.
/// Registro de solicitudes realizadas por cada usuario.
/// </summary>
private static readonly Dictionary<string, (DateTime Timestamp, int RequestCount)> _userRequestLog = new();


/// <summary>
/// Limite.
/// Límite máximo de solicitudes permitidas por intervalo.
/// </summary>
internal static int RequestLimit { get; set; }


/// <summary>
/// Tiempo de bloqueo.
/// Duración del intervalo en el cual se contabilizan las solicitudes.
/// </summary>
internal static TimeSpan TimeSpan { get; set; }
internal static TimeSpan TimeWindow { get; set; }


/// <summary>
/// AL invocar.
/// Procesa la solicitud y aplica el límite de solicitudes configurado.
/// </summary>
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)
{
Expand All @@ -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);
}


/// <summary>
/// Obtener el primary id del token JWT.
/// Obtiene el identificador primario del token JWT.
/// </summary>
/// <param name="token">Token a validar.</param>
private static string GetPrimaryId(string? token)
Expand All @@ -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 "";

Expand Down