web-dev-qa-db-fra.com

Comment limiter les demandes dans une API Web?

J'essaie d'implémenter la limitation de requête via les éléments suivants: 

Meilleur moyen d'implémenter la limitation de requête dans ASP.NET MVC?

J'ai intégré ce code dans ma solution et décoré un noeud final de contrôleur d'API avec l'attribut suivant: 

[Route("api/dothis/{id}")]
[AcceptVerbs("POST")]
[Throttle(Name = "TestThrottle", Message = "You must wait {n} seconds before accessing this url again.", Seconds = 5)]
[Authorize]
public HttpResponseMessage DoThis(int id) {...}

Ceci compile mais le code de l'attribut n'est pas touché et la limitation ne fonctionne pas. Je ne reçois aucune erreur cependant. Qu'est-ce que je rate?

37
RobVious

Vous semblez confondre les filtres d'action pour un contrôleur ASP.NET MVC et les filtres d'action pour un contrôleur API Web ASP.NET. Ce sont 2 classes complètement différentes:

Il semble que ce que vous avez montré est une action de contrôleur API Web (déclarée dans un contrôleur dérivé de ApiController). Ainsi, si vous souhaitez y appliquer des filtres personnalisés, ils doivent être dérivés de System.Web.Http.Filters.ActionFilterAttribute.

Alors allons-y et adaptons le code pour l'API Web:

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(HttpActionContext actionContext)
    {
        var key = string.Concat(Name, "-", GetClientIp(actionContext.Request));
        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.";
            }

            actionContext.Response = actionContext.Request.CreateResponse(
                HttpStatusCode.Conflict, 
                Message.Replace("{n}", Seconds.ToString())
            );
        }
    }
}

d'où provient la méthode GetClientIpthis post .

Vous pouvez maintenant utiliser cet attribut sur votre action de contrôleur API Web.

43
Darin Dimitrov

La solution proposée n'est pas exacte. Il y a au moins 5 raisons pour cela.

  1. Le cache ne fournit pas de contrôle d'inter-verrouillage entre les différents threads. Par conséquent, plusieurs demandes peuvent être traitées en même temps, ce qui introduit des appels supplémentaires en évitant le contrôle. 
  2. Le filtre est en cours de traitement 'trop tard dans le jeu' dans le pipeline des API Web. Par conséquent, de nombreuses ressources sont dépensées avant que vous ne décidiez que la demande ne doit pas être traitée. DelegatingHandler doit être utilisé car il peut être configuré pour s'exécuter au début du pipeline de l'API Web et couper la demande avant d'effectuer tout travail supplémentaire.
  3. Le cache Http lui-même est une dépendance qui pourrait ne pas être disponible avec les nouvelles exécutions, comme les options auto-hébergées. Il vaut mieux éviter cette dépendance.
  4. La mémoire cache dans l'exemple ci-dessus ne garantit pas sa survie entre les appels, car elle pourrait être supprimée en raison de la pression de la mémoire, notamment en raison d'une priorité faible.
  5. Bien que le problème ne soit pas trop grave, définir le statut de la réponse sur 'conflit' ne semble pas être la meilleure option. Il est préférable d'utiliser "429-trop de demandes" à la place.

Il existe de nombreux autres problèmes et obstacles cachés à résoudre lors de la mise en œuvre de la régulation. Il y a des options open source gratuites disponibles. Je recommande de regarder https://throttlewebapi.codeplex.com/ , par exemple.

47
lenny12345

WebApiThrottle est tout à fait le champion maintenant dans ce domaine. 

C'est super facile à intégrer. Ajoutez simplement ce qui suit à App_Start\WebApiConfig.cs:

config.MessageHandlers.Add(new ThrottlingHandler()
{
    // Generic rate limit applied to ALL APIs
    Policy = new ThrottlePolicy(perSecond: 1, perMinute: 20, perHour: 200)
    {
        IpThrottling = true,
        ClientThrottling = true,
        EndpointThrottling = true,
        EndpointRules = new Dictionary<string, RateLimits>
        { 
             //Fine tune throttling per specific API here
            { "api/search", new RateLimits { PerSecond = 10, PerMinute = 100, PerHour = 1000 } }
        }
    },
    Repository = new CacheRepository()
});

Il est également disponible sous forme de nuget portant le même nom.

27
Korayem

Revérifiez les instructions using dans votre filtre d'action. Lorsque vous utilisez un contrôleur API, veillez à référencer ActionFilterAttribute dans System.Web.Http.Filters et not celui dans System.Web.Mvc.

using System.Web.Http.Filters;
4
Ant P

J'utilise ThrottleAttribute pour limiter le débit d'appel de mon API d'envoi de messages courts, mais cela ne fonctionnait parfois pas. L'API peut avoir été appelé plusieurs fois jusqu'à ce que la logique de régulation fonctionne. Enfin, j'utilise System.Web.Caching.MemoryCache au lieu de HttpRuntime.Cache et le problème semble résolu.

if (MemoryCache.Default[key] == null)
{
    MemoryCache.Default.Set(key, true, DateTime.Now.AddSeconds(Seconds));
    allowExecute = true;
}
2
Bruce

Mes 2 cents ajoutent quelques informations supplémentaires pour 'clé' concernant les informations de requête sur les paramètres, de sorte qu'une requête de paramètre différente soit autorisée à partir d'une même adresse IP.

key = Name + clientIP + actionContext.ActionArguments.Values.ToString()

En outre, mon petit souci au sujet du "clientIP", est-il possible que deux utilisateurs différents utilisent le même fournisseur de services Internet a le même "clientIP"? Si oui, alors un client peut être étranglé à tort.

0
RyanShao