in ASP.NET

Implementando Rate Limit e Throttling com Web API usando WebApiThrottle

Com a diversificação de devices e plataformas (mobile, wearable, sites, etc) os dados de nossas aplicações, regras de negocio e ações estão sendo expostos utilizando API’s HTTP. O ASP.NET Web API é um excelente framework de desenvolvimento para serviços HTTP, possuindo recursos como roteamento model binding, serialização, segurança, hospedagem entre outros.

Uma necessidade bastante comum neste cenário é  de controle do uso destas APIs, como por exemplo o numero máximo de requests que um cliente pode realizar em um determinado range de tempo (rate limit, throttling). Para isto existem diversas maneiras de realizar este gerenciamento, como por exemplo proxies(que ficam como um

intermediário entre nossas APIs e o cliente, realizando o controle) e algumas bibliotecas. Existe até um serviço do Azure, o Azure API Management (PREVIEW) que possui estas funcionalidades e muitas outras como o de gerenciamento de autenticação e keys.

Recentemente tive que implementar este tipo de controle em um projeto pessoal e encontrei uma biblioteca chamada WebAPIThrottle (autor Stefan Prodan), que possui diversos recursos, e é implementada utilizando Handlers e Filtros e distribuída através de nuget: https://www.nuget.org/packages/WebApiThrottle/

Principais Recursos do WebAPIThrottle

Com esta biblioteca é possível configurar vários limites em diversos cenários, como por exemplo: controle baseado em ips, em chaves de autenticação, em urls/endpoints da API, tudo isto em diferentes ranges de tempo (numero máximo requests por segundo, minuto, hora, dia e semana). Além de recursos como armazenamento destas métricas de consumo e log.

Começando

Para começar, vou criar um projeto ASP.NET WebAPI e adicionar o projeto via nuget:

PM> Install-Package WebApiThrottle

Global throttling baseado em IP

Com esta configuração é possível restringir o numero de chamadas por um ip para todos os endpoints da API, com a configuração desta maneira, a soma das requisições é feita agrupando como um todo, em qualquer endpoint.

Para isso é necessário adicionar um MessageHandler no WebAPIConfig.cs, no caso é o ThrottlingHandler, no qual realizaremos todas as configurações, como limites por segundo, minuto e configurações gerais de rate limits

using System.Web.Http;
using WebApiThrottle;

namespace WebApplication13
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services
            config.MessageHandlers.Add(new ThrottlingHandler()
            {
                Policy = new ThrottlePolicy(perSecond: 1, perMinute: 2, perHour:400, perDay:4000, perWeek:5000)
                {
                    IpThrottling = true
                },
                Repository = new CacheRepository()
            });

            // Web API routes
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}

Notem que é passado um Repository para armazenar os dados de requisições, caso a WebAPI seja hospedada com Owin é necessário alterar o Repository, passando um objeto do tipo MemoryCacheRepository.

Endpoint throttling baseado em IP

Assim como é possível configurar o throttling para funcionar globalmente, é possível configurar o limite agrupando por endpoint. No exemplo abaixo é possível realizar requisições para diferentes endpoints, por exemplo você pode realizar duas requisições para o api/values e duas requisições para o api/values/1 dentro do mesmo minuto.

config.MessageHandlers.Add(new ThrottlingHandler()
{
    Policy = new ThrottlePolicy(perSecond: 1, perMinute: 2, perHour:400, perDay:4000, perWeek:5000)
    {
        IpThrottling = true,
        EndpointThrottling = true
    },
    Repository = new CacheRepository()
});

Endpoint throttling baseado em IP e Client Key

Outra configuração que é possível ser utilizada é considerando também o Client Key para agrupar o limite de requisições,  neste caso o header “Authorization-Token” é utilizado como Client Key.

config.MessageHandlers.Add(new ThrottlingHandler()
{
    Policy = new ThrottlePolicy(perSecond: 1, perMinute: 2, perHour:400, perDay:4000, perWeek:5000)
    {
        IpThrottling = true,
        EndpointThrottling = true,
        ClientThrottling = true
    },
    Repository = new CacheRepository()
});

IP e Client Key White-listing

Outro parâmetro que pode ser utilizado é a criação de WhiteList de Ips e ou de Clients, com elas, as requisições que tiveram origem nestes ips ou utilizarem a ClientKey definida não são armazenadas nem controladas.

config.MessageHandlers.Add(new ThrottlingHandler()
{
    Policy = new ThrottlePolicy(perSecond: 1, perMinute: 2, perHour:400, perDay:4000, perWeek:5000)
    {
        IpThrottling = true,
        EndpointThrottling = true,
        ClientThrottling = true,
        IpWhitelist = new List<string> { "::1", "192.168.0.0/24" },
        ClientWhitelist = new List<string> { "chave-admin" }
    },
    Repository = new CacheRepository()
});

Rate limits customizados por IP e Client Key

Com esta biblioteca também é possível configurar os limites de acordo com o Ip de origem ou o ClientKey que foi utilizado.

config.MessageHandlers.Add(new ThrottlingHandler()
{
    Policy = new ThrottlePolicy(perSecond: 1, perMinute: 2, perHour: 400, perDay: 4000, perWeek: 5000)
    {
        IpThrottling = true,
        EndpointThrottling = true,
        ClientThrottling = true,
        IpRules = new Dictionary<string, RateLimits>
        {
            { "192.168.1.1", new RateLimits { PerSecond = 1 } },
            { "192.168.2.0/24", new RateLimits { PerMinute = 30, PerHour = 30*60, PerDay = 30*60*24 } }
        },
        ClientRules = new Dictionary<string, RateLimits>
        {
            { "chave-admin-1", new RateLimits { PerMinute = 40, PerHour = 400 } },
            { "chave-admin-2", new RateLimits { PerDay = 2000 } }
        }
    },
    Repository = new CacheRepository()
});

Rate limits customizados por Endpoint

Os rate limits também podem ser configurados por endpoint ou por segmentos da url, como por exemplo abaixo que configuramos os limites para um endpoints especifico de busca.

config.MessageHandlers.Add(new ThrottlingHandler()
{
    Policy = new ThrottlePolicy(perSecond: 1, perMinute: 2, perHour: 400, perDay: 4000, perWeek: 5000)
    {
        IpThrottling = true,
        EndpointThrottling = true,
        ClientThrottling = true,
        EndpointRules = new Dictionary<string, RateLimits>
        {
            { "api/busca", new RateLimits { PerSecond = 10, PerMinute = 100, PerHour = 1000 } }
        }
    },
    Repository = new CacheRepository()
});

Customização para obter o Client Key (ex header diferente)

É possível criar uma objeto que herde de ThrottlingHandler e sobrescrever o método responsável por configurar o identity da requisição (SetIdentity) no exemplo abaixo eu trago o Client Key através de um header chamado Rodolfo-Key.

public class CustomThrottlingHandler : ThrottlingHandler
{
    protected override RequestIdentity SetIndentity(HttpRequestMessage request)
    {
        return new RequestIdentity()
        {
            ClientKey = request.Headers.GetValues("Rodolfo-Key").First(),
            ClientIp = base.GetClientIp(request).ToString(),
            Endpoint = request.RequestUri.AbsolutePath
        };
    }
}

Rate limits por atributos

Nas primeira configurações que eu mostrei adicionamos o controle através do uso de MessageHandler, também é possível adicionar estas configurações através de filtros e  atributos para configurar os limites no próprio Controller. Porém por configuração do momento de execução do Handler e do Filter, é preferível utilizar o MessageHandler, que é executado antes do dispacher do Controller (http://www.asp.net/posters/web-api/asp.net-web-api-poster-grayscale.pdf).

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    filters.Add(new HandleErrorAttribute());

    filters.Add(new ThrottlingFilter()
    {
        Policy = new ThrottlePolicy(perSecond: 1, perMinute: 20,
        perHour: 200, perDay: 2000, perWeek: 10000)
        {
            IpThrottling = true,
            ClientThrottling = true,
            EndpointThrottling = true
        }
    });
}

Abaixo um exemplo de configuração via atributos

namespace WebApplication13.Controllers
{
    [EnableThrottling(PerSecond = 2)]
    public class ValuesController : ApiController
    {
        // GET api/values
        [EnableThrottling(PerSecond = 1, PerMinute = 30, PerHour = 100)]
        public IEnumerable<string> Get()
        {
            return new string[] {"value1", "value2"};
        }

        // GET api/values/5
        [DisableThrotting]
        public string Get(int id)
        {
            return "value";
        }
    }
}

Bom, a ideia deste post é mostrar este excelente projeto, todo o código fonte dele está disponível no GitHub: https://github.com/stefanprodan/WebApiThrottle .

Espero que este post seja útil, estou a disposição para dúvidas, criticas e sugestões

abs

Rodolfo

  • Eduardo Cucharro

    Sensacional Rods, parabéns!

  • Cleber Dantas

    Bem legal esse esquema… Aliás parabéns pelo post @RodolfoFadino:disqus

    Só duas observações…

    A primeira é que para implementação desse nuget em um ambiente que tenha mais de um nó respondendo pela API já será necessário implementar um IThrottleRepository apontando para algum lugar “centralizador”, caso contrário as coisas simplesmente não funcionaram corretamente.

    O ponto dois é que no final das contas é bem estranho tratar rate limit (pelo menos pra mim) dentro da própria aplicação, pois um dos motivos de se ter isso é justamente evitar sobrecarregar a API com chamadas que ela nem deveria atender, por isso acho que o uso de proxy pode ser que faça mais sentido.

    Mas de qualquer forma o projeto é bem legal, acho que até vou dar um fork e implementar um dashboardzinho.

    Abraços!

  • rodrigo ratan

    e ae Rodolfo, tentei curtir e compartilhar mas o facebook encontra um 404 ao tentar acessar o preview do seu site….

  • http://www.rodolfofadino.com.br/ Rodolfo Fadino

    Opa, tudo bem Rodrigo, o Facebook fez o cache de um share que eu testei antes de publicar, agora eu alterei um pouco a url e deve funcionar, abs

  • rodrigo ratan

    valeu, agora rolou! abs

  • http://www.francispires.com.br Francis Pires

    throttling no título está errado, ótimo artigo