Лучший способ реализовать регулирование запросов в ASP.NET MVC?
Мы экспериментируем с различными способами регулирования действий пользователя в данный период времени:
- Ограничить вопрос / ответ постов
- Ограничение правок
- Ограничить поиск каналов
В настоящее время мы используем Cache для простой вставки записи активности пользователя - если эта запись существует, если / когда пользователь выполняет ту же самую активность, мы ограничиваемся.
Использование кэша автоматически дает нам устаревшую очистку данных и скользящие окна активности пользователей, но проблема масштабирования может быть проблемой.
Каковы другие способы обеспечения эффективного регулирования запросов / действий пользователя (акцент на стабильность)?
6 ответов
Вот общая версия того, что мы использовали в переполнении стека в прошлом году:
/// <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;
}
}
}
Пример использования:
[Throttle(Name="TestThrottle", Message = "You must wait {n} seconds before accessing this url again.", Seconds = 5)]
public ActionResult TestThrottle()
{
return Content("TestThrottle executed");
}
ASP.NET Cache здесь работает как первоклассный - используя его, вы получаете автоматическую очистку ваших записей газа. И с нашим растущим трафиком, мы не видим, что это проблема на сервере.
Не стесняйтесь давать отзывы об этом методе; когда мы сделаем Stack Overflow лучше, вы исправите Ewok еще быстрее:)
У Microsoft есть новое расширение для IIS 7, которое называется "Динамическое расширение ограничений IP для IIS 7.0 - бета-версия".
"Динамические ограничения IP для IIS 7.0 - это модуль, который обеспечивает защиту от атак типа" отказ в обслуживании "и атак" грубой силы "на веб-сервер и веб-сайты. Такая защита обеспечивается путем временной блокировки IP-адресов клиентов HTTP, которые выполняют необычно большое количество одновременных запросов или которые делают большое количество запросов в течение небольшого периода времени. " http://learn.iis.net/page.aspx/548/using-dynamic-ip-restrictions/
Пример:
Если вы установите критерии для блокировки после X requests in Y milliseconds
или же X concurrent connections in Y milliseconds
IP-адрес будет заблокирован для Y milliseconds
тогда запросы будут снова разрешены.
Мы используем технику, заимствованную из этого URL http://www.codeproject.com/KB/aspnet/10ASPNetPerformance.aspx, не для регулирования, а для отказа в обслуживании бедного человека (DOS). Это также основано на кеше и может быть похоже на то, что вы делаете. Вы душите, чтобы предотвратить атаки DOS? Маршрутизаторы, безусловно, могут быть использованы для уменьшения DOS; Как вы думаете, маршрутизатор может справиться с регулированием, которое вам нужно?
Мне потребовалось некоторое время, чтобы разработать эквивалент для .NET 5+ (ранее .NET Core), так что вот отправная точка.
Старый способ кэширования ушел и был заменен
Microsoft.Extensions.Caching.Memory
с IMemoryCache .
Я еще немного выделил, так что вот что вам нужно...
Класс управления кэшем
Я добавил все это здесь, чтобы вы могли видеть операторы использования.
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://docs.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;
}
}
}
Сам атрибут
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 или Program.cs — регистрация служб в DI
В этом примере используется Startup.cs/ConfigureServices — поместите код где-нибудь после
AddControllersWithViews
).
Для проекта, созданного в .NET6+, я думаю, вы бы добавили эквивалент между
builder.Services.AddRazorPages();
а также
var app = builder.Build();
в программе.cs.
services
было бы
builder.Services
.
Если вы неправильно разместите этот код, кеш будет пуст каждый раз, когда вы его проверяете.
// 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 THIS THE RIGHT SIZE FOR YOU! */
CompactionPercentage = .3,
ExpirationScanFrequency = TimeSpan.FromSeconds(30),
});
services.AddSingleton<IThrottleCache, ThrottleCache>();
Пример использования
[HttpGet, Route("GetTest")]
[ThrottleByIPAddress(Name = "MyControllerGetTest", Seconds = 5)]
public async Task<ActionResult<string>> GetTest()
{
return "Hello world";
}
Чтобы помочь понять кэширование в .NET 5+, я также сделал демонстрацию консоли кэширования .
Создал ThrottlingTroll — мой подход к регулированию/ограничению скорости в ASP.NET Core.
Он похож на AspNetCoreRateLimit Стефана Продана ипромежуточное ПО для ограничения скорости ASP.NET 7 , но имеет преимущества:
- Регулирование как входящего, так и исходящего трафика ( выход означает, что ваш специально настроенный HttpClient не будет выполнять более N запросов в секунду, а вместо этого будет сам создавать код состояния 429).
- Распределенные хранилища счетчиков ставок (включая, помимо прочего, Redis).
- Динамическая (ре)конфигурация — позволяет настраивать лимиты без перезапуска службы.
- Распространение 429 статусов от выхода к входу.
Поскольку ответы на этот вопрос, получившие большое количество голосов, слишком устарели, я делюсь последним решением, которое сработало для меня.
Я попытался использовать ограничения динамического IP-адреса, как указано в ответе на этой странице, но когда я попытался использовать это расширение, я обнаружил, что это расширение было прекращено Microsoft, и на странице загрузки они четко написали следующее сообщение.
Microsoft has discontinued the Dynamic IP Restrictions extension and this download is no longer available.
Поэтому я провел дополнительные исследования и обнаружил, что ограничения динамического IP-адреса теперь по умолчанию включены в IIS 8.0 и более поздние версии. Приведенная ниже информация получена со страницы Microsoft Dynamic IP Restrictions.
В IIS 8.0 Microsoft расширила встроенную функциональность, включив в нее несколько новых функций:
- Динамическая фильтрация IP-адресов, которая позволяет администраторам настроить свой сервер для блокировки доступа для IP-адресов, которые превышают указанное количество запросов.
- Функции фильтрации IP-адресов теперь позволяют администраторам указывать поведение, когда IIS блокирует IP-адрес, поэтому запросы от вредоносных клиентов могут быть прерваны сервером вместо того, чтобы возвращать клиенту ответы HTTP 403.6.
- IP-фильтрация теперь имеет режим прокси, который позволяет блокировать IP-адреса не только по IP-адресу клиента, который видит IIS, но и по значениям, полученным в HTTP-заголовке x-forwarded-for.
Для получения пошаговых инструкций по внедрению динамических ограничений IP-адресов перейдите по ссылке ниже:
Я надеюсь, что это поможет кому-то, застрявшему в подобной проблеме.