¿La mejor manera de implementar la limitación de solicitudes en ASP.NET MVC?
Estamos experimentando con varias formas de limitar las acciones de los usuarios en un período de tiempo determinado :
- Limitar publicaciones de preguntas/respuestas
- Limitar ediciones
- Limitar las recuperaciones de feeds
Por el momento, estamos usando la caché para simplemente insertar un registro de la actividad del usuario; si ese registro existe, si/cuando el usuario realiza la misma actividad, lo limitamos.
El uso de la caché nos brinda automáticamente limpieza de datos obsoletos y ventanas deslizantes de actividad de los usuarios, pero cómo se escalará podría ser un problema.
¿Cuáles son algunas otras formas de garantizar que las solicitudes/acciones de los usuarios puedan limitarse de manera efectiva (énfasis en la estabilidad)?
Aquí hay una versión genérica de lo que hemos estado usando en Stack Overflow durante el año pasado:
/// <summary>
/// Decorates any MVC route that needs to have client requests limited by time.
/// </summary>
/// <remarks>
/// Uses the current System.Web.Caching.Cache to store each client request to the decorated route.
/// </remarks>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ThrottleAttribute : ActionFilterAttribute
{
/// <summary>
/// A unique name for this Throttle.
/// </summary>
/// <remarks>
/// We'll be inserting a Cache record based on this name and client IP, e.g. "Name-192.168.0.1"
/// </remarks>
public string Name { get; set; }
/// <summary>
/// The number of seconds clients must wait before executing this decorated route again.
/// </summary>
public int Seconds { get; set; }
/// <summary>
/// A text message that will be sent to the client upon throttling. You can include the token {n} to
/// show this.Seconds in the message, e.g. "Wait {n} seconds before trying again".
/// </summary>
public string Message { get; set; }
public override void OnActionExecuting(ActionExecutingContext c)
{
var key = string.Concat(Name, "-", c.HttpContext.Request.UserHostAddress);
var allowExecute = false;
if (HttpRuntime.Cache[key] == null)
{
HttpRuntime.Cache.Add(key,
true, // is this the smallest data we can have?
null, // no dependencies
DateTime.Now.AddSeconds(Seconds), // absolute expiration
Cache.NoSlidingExpiration,
CacheItemPriority.Low,
null); // no callback
allowExecute = true;
}
if (!allowExecute)
{
if (String.IsNullOrEmpty(Message))
Message = "You may only perform this action every {n} seconds.";
c.Result = new ContentResult { Content = Message.Replace("{n}", Seconds.ToString()) };
// see 409 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
c.HttpContext.Response.StatusCode = (int)HttpStatusCode.Conflict;
}
}
}
Uso de muestra:
[Throttle(Name="TestThrottle", Message = "You must wait {n} seconds before accessing this url again.", Seconds = 5)]
public ActionResult TestThrottle()
{
return Content("TestThrottle executed");
}
El caché ASP.NET funciona como un campeón aquí: al usarlo, obtienes una limpieza automática de tus entradas de aceleración. Y con nuestro creciente tráfico, no vemos que esto sea un problema en el servidor.
No dude en dar su opinión sobre este método; cuando mejoramos Stack Overflow, obtendrás tu solución de Ewok aún más rápido :)
Microsoft tiene una nueva extensión para IIS 7 llamada Extensión de restricciones de IP dinámicas para IIS 7.0 - Beta.
"Las Restricciones de IP dinámicas para IIS 7.0 es un módulo que proporciona protección contra denegación de servicio y ataques de fuerza bruta en servidores y sitios web. Dicha protección se proporciona bloqueando temporalmente las direcciones IP de los clientes HTTP que realizan un número inusualmente alto de solicitudes simultáneas. o que hacen un gran número de solicitudes en un corto período de tiempo." http://learn.iis.net/page.aspx/548/using-dynamic-ip-restrictions/
Ejemplo:
Si establece los criterios para bloquear después X requests in Y milliseconds
o X concurrent connections in Y milliseconds
se bloqueará la dirección IP, Y milliseconds
se permitirán nuevamente las solicitudes.
Usamos la técnica tomada de esta URL http://www.codeproject.com/KB/aspnet/10ASPNetPerformance.aspx , no para acelerar, sino para la denegación de servicio (DOS) de un pobre. Esto también se basa en caché y puede ser similar a lo que estás haciendo. ¿Está acelerando para evitar ataques de DOS? Ciertamente se pueden utilizar enrutadores para reducir DOS; ¿Crees que un enrutador podría manejar la aceleración que necesitas?
Me tomó algo de tiempo encontrar un equivalente para .NET 5+ (anteriormente .NET Core), así que aquí hay un punto de partida.
La antigua forma de almacenamiento en caché desapareció y fue reemplazada Microsoft.Extensions.Caching.Memory
por IMemoryCache .
Lo separé un poco más, así que esto es lo que necesitas...
La clase de gestión de caché
Agregué todo aquí, para que pueda ver las declaraciones de uso.
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Primitives;
using System;
using System.Threading;
namespace MyWebApplication
{
public interface IThrottleCache
{
bool AddToCache(string key, int expriryTimeInSeconds);
bool AddToCache<T>(string key, T value, int expriryTimeInSeconds);
T GetFromCache<T>(string key);
bool IsInCache(string key);
}
/// <summary>
/// A caching class, based on the docs
/// https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-6.0
/// Uses the recommended library "Microsoft.Extensions.Caching.Memory"
/// </summary>
public class ThrottleCache : IThrottleCache
{
private IMemoryCache _memoryCache;
public ThrottleCache(IMemoryCache memoryCache)
{
_memoryCache = memoryCache;
}
public bool AddToCache(string key, int expriryTimeInSeconds)
{
bool isSuccess = false; // Only a success if a new value gets added.
if (!IsInCache(key))
{
var cancellationTokenSource = new CancellationTokenSource(
TimeSpan.FromSeconds(expriryTimeInSeconds));
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSize(1)
.AddExpirationToken(
new CancellationChangeToken(cancellationTokenSource.Token));
_memoryCache.Set(key, DateTime.Now, cacheEntryOptions);
isSuccess = true;
}
return isSuccess;
}
public bool AddToCache<T>(string key, T value, int expriryTimeInSeconds)
{
bool isSuccess = false;
if (!IsInCache(key))
{
var cancellationTokenSource = new CancellationTokenSource(
TimeSpan.FromSeconds(expriryTimeInSeconds));
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(DateTimeOffset.Now.AddSeconds(expriryTimeInSeconds))
.SetSize(1)
.AddExpirationToken(
new CancellationChangeToken(cancellationTokenSource.Token));
_memoryCache.Set<T>(key, value, cacheEntryOptions);
isSuccess = true;
}
return isSuccess;
}
public T GetFromCache<T>(string key)
{
return _memoryCache.Get<T>(key);
}
public bool IsInCache(string key)
{
var item = _memoryCache.Get(key);
return item != null;
}
}
}
El atributo en sí
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Net;
namespace MyWebApplication
{
/// <summary>
/// Decorates any MVC route that needs to have client requests limited by time.
/// Based on how they throttle at stack overflow (updated for .NET5+)
/// https://stackoverflow.com/questions/33969/best-way-to-implement-request-throttling-in-asp-net-mvc/1318059#1318059
/// </summary>
/// <remarks>
/// Uses the current System.Web.Caching.Cache to store each client request to the decorated route.
/// </remarks>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ThrottleByIPAddressAttribute : ActionFilterAttribute
{
/// <summary>
/// The caching class (which will be instantiated as a singleton)
/// </summary>
private IThrottleCache _throttleCache;
/// <summary>
/// A unique name for this Throttle.
/// </summary>
/// <remarks>
/// We'll be inserting a Cache record based on this name and client IP, e.g. "Name-192.168.0.1"
/// </remarks>
public string Name { get; set; }
/// <summary>
/// The number of seconds clients must wait before executing this decorated route again.
/// </summary>
public int Seconds { get; set; }
/// <summary>
/// A text message that will be sent to the client upon throttling. You can include the token {n} to
/// show this.Seconds in the message, e.g. "Wait {n} seconds before trying again".
/// </summary>
public string Message { get; set; } = "You may only perform this action every {n} seconds.";
public override void OnActionExecuting(ActionExecutingContext c)
{
if(_throttleCache == null)
{
var cache = c.HttpContext.RequestServices.GetService(typeof(IThrottleCache));
_throttleCache = (IThrottleCache)cache;
}
var key = string.Concat(Name, "-", c.HttpContext.Request.HttpContext.Connection.RemoteIpAddress);
var allowExecute = _throttleCache.AddToCache(key, Seconds);
if (!allowExecute)
{
if (String.IsNullOrEmpty(Message))
Message = "You may only perform this action every {n} seconds.";
c.Result = new ContentResult { Content = Message.Replace("{n}", Seconds.ToString()) };
// see 409 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
c.HttpContext.Response.StatusCode = (int)HttpStatusCode.Conflict;
}
}
}
}
Startup.cs o Program.cs: registre los servicios con DI
Este ejemplo utiliza Startup.cs/ConfigureServices: coloque el código en algún lugar después AddControllersWithViews
).
Para un proyecto creado en .NET6+, creo que agregarías el equivalente entre builder.Services.AddRazorPages();
y var app = builder.Build();
en program.cs. services
sería builder.Services
.
Si no coloca correctamente este código, el caché estará vacío cada vez que lo verifique.
// The cache for throttling must be a singleton and requires IMemoryCache to be set up.
// Place it after AddControllersWithViews or AddRazorPages as they build a cache themselves
// Need this for IThrottleCache to work.
services.AddMemoryCache(_ => new MemoryCacheOptions
{
SizeLimit = 1024, /* TODO: CHECK THIS IS THE RIGHT SIZE FOR YOU! */
CompactionPercentage = .3,
ExpirationScanFrequency = TimeSpan.FromSeconds(30),
});
services.AddSingleton<IThrottleCache, ThrottleCache>();
Uso de ejemplo
[HttpGet, Route("GetTest")]
[ThrottleByIPAddress(Name = "MyControllerGetTest", Seconds = 5)]
public async Task<ActionResult<string>> GetTest()
{
return "Hello world";
}
Para ayudar a comprender el almacenamiento en caché en .NET 5+, también hice una demostración de la consola de almacenamiento en caché .