¿La mejor manera de implementar la limitación de solicitudes en ASP.NET MVC?

Resuelto Jarrod Dixon asked hace 16 años • 6 respuestas

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)?

Jarrod Dixon avatar Aug 29 '08 11:08 Jarrod Dixon
Aceptado

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 :)

Jarrod Dixon avatar Aug 23 '2009 08:08 Jarrod Dixon

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 millisecondso X concurrent connections in Y millisecondsse bloqueará la dirección IP, Y millisecondsse permitirán nuevamente las solicitudes.

notandy avatar Feb 25 '2009 04:02 notandy

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?

Rob Kraft avatar Nov 14 '2008 20:11 Rob Kraft

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.Memorypor 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. servicesserí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é .

JsAndDotNet avatar Mar 28 '2022 11:03 JsAndDotNet