Limiter les abus sur les routes sensibles
Objectif : Imposer des limites adaptées aux appels d’API et aux demandes d’export.
Une route d’export peut consommer des ressources longtemps après la réception de la requête. Une route de facturation, elle, peut être appelée en rafale et provoquer une accumulation de tâches. Vous allez définir des limites distinctes pour ces usages et refuser proprement les appels qui les dépassent.
La limitation de débit borne le nombre d’appels permis pendant une fenêtre donnée, par exemple cinq exports par minute. Un quota par utilisateur borne plutôt une quantité métier sur une période, comme un nombre maximal d’exports par heure. Ces règles ne remplacent ni l’authentification ni l’autorisation : elles s’appliquent après l’identification de l’appelant, afin de distinguer les utilisateurs.
Définir une limite indépendante des routes
Le middleware ci-dessous reçoit ses règles et son stockage par injection. Le stockage doit fournir une opération atomique increment : elle crée ou augmente un compteur associé à une clé et renvoie sa valeur ainsi que l’instant d’expiration. Cette opération évite que deux requêtes concurrentes lisent la même valeur puis écrasent chacune le compteur de l’autre.
export function creerLimitation({ store, regles, horloge = Date.now }) {
if (!store || typeof store.increment !== 'function') {
throw new TypeError('Le stockage doit fournir increment().');
}
return function limiter({ nom, cle, limite, fenetreMs }) {
if (!nom || typeof cle !== 'function') {
throw new TypeError('Chaque limite exige un nom et une fonction cle.');
}
if (!Number.isInteger(limite) || limite < 1 ||
!Number.isInteger(fenetreMs) || fenetreMs < 1) {
throw new TypeError('La limite et la fenêtre doivent être positives.');
}
return async function middlewareLimitation(req, res, next) {
try {
const identifiant = cle(req);
if (!identifiant) {
return res.status(401).json({ erreur: 'Identité requise.' });
}
const maintenant = horloge();
const compteur = await store.increment({
cle: `${nom}:${identifiant}`,
fenetreMs,
maintenant
});
const resteMs = Math.max(0, compteur.expireA - maintenant);
const restant = Math.max(0, limite - compteur.valeur);
res.set('RateLimit-Limit', String(limite));
res.set('RateLimit-Remaining', String(restant));
res.set('RateLimit-Reset', String(Math.ceil(resteMs / 1000)));
if (compteur.valeur > limite) {
res.set('Retry-After', String(Math.max(1, Math.ceil(resteMs / 1000))));
return res.status(429).json({ erreur: 'Limite atteinte. Réessayez plus tard.' });
}
return next();
} catch (erreur) {
return next(erreur);
}
};
};
}
export function creerLimiteurs({ store, configuration }) {
const limiter = creerLimitation({ store, regles: configuration.regles });
return {
appelsApi: limiter({
nom: 'api',
cle: req => req.user?.id,
limite: configuration.regles.appelsApi.limite,
fenetreMs: configuration.regles.appelsApi.fenetreMs
}),
exports: limiter({
nom: 'exports',
cle: req => req.user?.id,
limite: configuration.regles.exports.limite,
fenetreMs: configuration.regles.exports.fenetreMs
})
};
}La clé associe le compteur à l’utilisateur authentifié ; sans identité, le middleware renvoie 401 au lieu de créer un quota commun pour tous les visiteurs. Le préfixe nom sépare les compteurs d’API et d’exports, même si les deux s’appuient sur le même utilisateur. La configuration est fournie au démarrage par l’application : définissez-y les valeurs de production et de développement, sans inscrire de paramètres d’environnement dans ce fichier.
Choisir des limites adaptées à l’opération
Une limite générale protège le service contre les rafales, mais elle ne tient pas compte du coût des actions. Appliquez un plafond plus bas à la route d’export qu’aux consultations légères, puis ajustez-le en fonction de la capacité du service et de l’expérience attendue. La limitation doit être montée après le middleware d’authentification pour que req.user.id soit disponible ; le contrôle de permission reste distinct et doit aussi être appliqué à la route.
Le statut HTTP 429 signifie que l’appel dépasse une limite ; Retry-After indique un délai minimal avant une nouvelle tentative. Les en-têtes RateLimit-Limit, RateLimit-Remaining et RateLimit-Reset rendent la décision compréhensible pour un client API. Le client ne doit pas relancer immédiatement en boucle : il attend le délai annoncé, puis réévalue sa demande.
Le module relaie les erreurs du stockage vers la gestion d’erreurs Express au lieu de laisser la requête bloquée ou de prétendre qu’elle est autorisée. Cette décision fait échouer l’appel lorsque la protection ne peut pas déterminer le compteur. Pour une API qui doit rester disponible en cas d’indisponibilité du stockage, définissez explicitement une stratégie de repli et mesurez ses effets ; ne désactivez pas silencieusement la limite.
À retenir
- Définissez des limites distinctes selon la fréquence et le coût des opérations.
- Appliquez les compteurs à l’identité authentifiée et configurez leurs valeurs par environnement.
- Utilisez un stockage partagé et atomique lorsque plusieurs instances servent l’API.
- Répondez avec 429 et Retry-After lorsque la limite est dépassée.
- Traitez explicitement les erreurs du stockage et évitez toute désactivation silencieuse de la protection.
Dans la prochaine leçon, vous assemblerez le flux de facturation asynchrone et transmettrez l’identifiant de corrélation entre les composants.