DEV Community

Cover image for Implementação de RateLimit em Aplicações .NET 8
Bruno Silva
Bruno Silva

Posted on

Implementação de RateLimit em Aplicações .NET 8

Hoje vamos falar um pouco sobre Ratelimit e aproveitar para mostrar como fazer a implementação em uma aplicação Net 8.

Antes de ir pra código precisamos entender.

O que é RateLimit?

Este termo "rate limit" é referente a um mecanismo usado para controlar a taxa de solicitações de requisições que um usuário pode realizar em um determinado período de tempo.

Por exemplo, em APIs podemos impor limites de taxa para evitar que um único cliente ou aplicativo sobrecarregue o servidor com um grande número de solicitações em um curto período de tempo. E isso pode ser feito através do "Rate Limit".

Em resumo, o rate limit é uma técnica utilizada para garantir que um serviço ou recurso não seja sobrecarregado, equilibrando a carga e garantindo um desempenho consistente e justo para todos os usuários, limitando assim o número de solicitações que podem ser feitas por segundo, minuto, hora etc.

Os bloqueios podem ser feitos por usuário , ip de origem, endpoint , enfim as possibilidades são inúmeras.

Implementação em .NET 8

A partir do .net 7 a Microsoft disponibilizou um middleware nativo para implementação de limite de requisições.

Foram disponibilizadas 4 alternativas de limites:

  • Fixed window - (Janela fixa de tempo, neste cenário limitamos a quantidade de requisições em um determinado tempo. Ex: 5 requisições a cada 10 segundos )

  • Sliding window - (Janela variável de tempo. É semelhante ao limitador de janela fixa, mas adiciona segmentos por janela. A janela desliza um segmento a cada intervalo de segmento. O intervalo de segmento é (tempo de janela)/(segmentos por janela))

  • Token bucket - Simplificando é como se fosse um balde de tokens. Você tem uma quantidade de tokens em um balde e a cada requisição um token é retirado. A cada período os tokens também são adicionados e se em certo momento não tiver token disponível é porque atingiu a capacidade máxima.

  • Concurrency - Limitação por simultaneidade, ele limita a quantidade simultâneas de requisições.

Caso queira se aprofundar em mais detalhes segue o documentação da Microsoft

https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?view=aspnetcore-8.0#fixed

Então para exemplificar vamos criar uma Api em .net 8 com alguns endpoints para simular.

Vamos focar nas opções mais usadas que é a fixed window e concurrency e vamos deixar nossa Api com 3 configurações distintas.

Bora pro código?

Para habilitar o Ratelimit na sua Api vamos ter que adicionar o método AddRateLimiter no builder.Service da sua classe program.

Neste método é possível configurar várias opções do ratelimit, a primeira configuração que vamos fazer é padronização do retorno do status code da requisição quando tivermos o limite atingindo. Para essa configuração basta associar o status 429 (too many request) a configurações opt.RejectionStatusCode.

Vamos separar nosso RateLimit em 3 opções.

  • Global: Vamos ter uma limitação por usuário e endpoint onde a quantidade limite de requisições seja 120 requisições por minuto.

  • Politica Por IP: Criaremos uma politica onde a limitação será por usuário e endpoint e a quantidade de requisições não pode passar de 50 requisições por minuto.

  • Politica Por Concorrência: vamos criar uma limitação onde o endpoint só pode receber 1 requisição por vez.

Politica Global:
A configuração GlobalLimiter será definida com o tipo Fixed Window , no GetFixedWindowLimiter precisamos ajustar a partition key (essa partition key é como será agrupada as nossas requisições. No nosso exemplo vamos agrupar por usuário logado e endpoint).
No retorno desse método vamos definir as configurações.

  • AutoReplenishment = true, //autoincremento
  • PermitLimit = 120, // limite de requisição
  • QueueLimit = 0,// limite de requisições em fila
  • Window =TimeSpan.FromSeconds(60) // janela de tempo

Politica Por Ip:
Um pouco mais abaixo vamos adicionar uma politica nova como o código addPolicy, definindo o nome da politica e colocando o partitionKey por Ip e Endpoint. Vamos retornar as configurações de 50 requisições por minuto.

Politica por Concorrência:
A configuração por concorrência segue os mesmos passos com algumas modificações. A adição da politica é igual então vamos fazer o addPolicy com o nome concurrency, porem o RateLimitPartition precisar ser o método GetConcurrencyLimiter. No GetConcurrencyLimiter vamos definir a partitionKey apenas por endpoint e o PermitiLimit vamos definir apenas 1, para que a limite de requisição seja apenas 1. O QueueLimit será 0 para não enfileirar nenhum request.

A ultima configuração que vamos fazer é a padronização da mensagem de retorno. Fazemos isso no OnRejected. Desta forma temos uma mensagem mais amigável de retorno.

Após as configurações feitas temos que ativar o rateLimite adicionando o codigo app.UseRateLimiter().

Lembrando que a adição desse trecho de código tem que ficar após o app.UseRouting(). Se colocado antes o Rate limit pode não funcionar.

A classe program vai ficar com as informações abaixo


builder.Services.AddRateLimiter(opt =>
{
    opt.RejectionStatusCode = StatusCodes.Status429TooManyRequests; //força o retorno 429 de too many requests
    opt.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: $"{httpContext.User.Identity?.Name}_{httpContext.Request.Path}",
            factory: partion =>
            {
                return new FixedWindowRateLimiterOptions
                {
                    AutoReplenishment = true, //autoincremento 
                    PermitLimit = 120, // limite de requisição
                    QueueLimit = 0,// limite de requisições em fila
                    Window = TimeSpan.FromSeconds(60) // janela de tempo
                };
            })
    );

    opt.AddPolicy("ip", httpContext =>
       RateLimitPartition.GetFixedWindowLimiter(
           partitionKey: $"{httpContext.Request.Path}_{httpContext.Connection.RemoteIpAddress}",
           factory: partition =>
           {
               return new FixedWindowRateLimiterOptions
               {
                   AutoReplenishment = true,
                   PermitLimit = 50,
                   QueueLimit = 0,
                   Window = TimeSpan.FromSeconds(60)
               };
           }));
    opt.AddPolicy("concurrency", httpContext =>
        RateLimitPartition.GetConcurrencyLimiter(
            partitionKey: httpContext.Request.Path,
            factory: partition =>
            {
                return new ConcurrencyLimiterOptions { 
                    PermitLimit = 1,
                    QueueLimit = 0,
                };
            }));


    opt.OnRejected = async (context, token) =>
    {
        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAtfer))
            await context.HttpContext.Response.WriteAsync($"O limite de requisições foi atingifo tente novamente daqui {retryAtfer.TotalSeconds} segundos ");
        else if (context.Lease.TryGetMetadata(MetadataName.ReasonPhrase, out var reasonPhrase))
            await context.HttpContext.Response.WriteAsync($"O limite de requisições simultâneas foi atingido,tente novamente mais tarde");
        else
            await context.HttpContext.Response.WriteAsync("O limite de requisições foi atingido, tente novamente mais tarde");
    };
});

var app = builder.Build();

app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseCors(x => x
    .AllowAnyOrigin()
    .AllowAnyMethod()
    .AllowAnyHeader()
);

app.UseRateLimiter();
app.UseEndpoints(endpoints => { _ = endpoints.MapControllers(); });
app.UseResponseCompression();
app.Run();


Enter fullscreen mode Exit fullscreen mode

Agora temos que configurar as nossas Controllers, lembrando que como temos uma politica global todas os endpoints já nasceram com a politica Global aplicadaa. Vamos ter que nos preocupar em configurar apenas as politicas de concorrência e por Ip

No primeiro endpoint temos o exemplo da rateLimite global, onde não há nenhuma atributo vinculado.

No Segundo endpoint foi adicionado o atributo [EnableRateLimiting("ip")], aqui o endpoint é forçado a usar a politica por IP.

No terceiro Endpoint foi adicionado o atributo [EnableRateLimiting("concurrency")], aqui o endpoint é forçado a usar a politica por concorrência.

Código Controller

  public class ExamplesController : ControllerBase
  {


      [HttpPost]
      [Route("rate-limiting-global")]
      public IActionResult ReteLimitingGlobal()
      {
          Thread.Sleep(10000);
          return Ok();
      }

      [HttpGet]
      [EnableRateLimiting("ip")]
      [Route("rate-limiting")]
      public IActionResult RateLimitingIP()
      {
          Thread.Sleep(10000);
          return Ok();
      }

      [HttpPost]
      [EnableRateLimiting("concurrency")]
      [Route("rate-limiting-post")]
      public IActionResult ReteLimitingPost()
      {
          Thread.Sleep(10000);
          return Ok();
      }
Enter fullscreen mode Exit fullscreen mode

Ex: de uma chamada com limite atingido no Swagger

Image description

Simples né com apenas essas configurações você consegue ter N possibilidades de deixar sua api mais segura com controle de requisições. Isso ajuda a evitar ataques DDoS e ataques de força bruta.

Espero que tenham gostado e até a próxima!

Top comments (2)

Collapse
 
jaozincosta profile image
João Costa

Muito fera

Collapse
 
fehsilva profile image
Felipe

Muito bom!