src/EventSubscriber/LoginThrottleSubscriber.php line 34

Open in your IDE?
  1. <?php
  2. namespace App\EventSubscriber;
  3. use Psr\Cache\CacheItemPoolInterface;
  4. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  5. use Symfony\Component\HttpFoundation\Response;
  6. use Symfony\Component\HttpKernel\Event\RequestEvent;
  7. use Symfony\Component\HttpKernel\KernelEvents;
  8. /**
  9.  * SÉCURITÉ : limite les tentatives de connexion (anti force brute).
  10.  * Maximum 15 appels aux routes de connexion par adresse IP et par fenêtre de 15 minutes.
  11.  * L'adresse IP vient uniquement de REMOTE_ADDR (les en-têtes X-Forwarded-For sont falsifiables).
  12.  */
  13. class LoginThrottleSubscriber implements EventSubscriberInterface
  14. {
  15.     public const MAX_ATTEMPTS = 15;
  16.     public const WINDOW_SECONDS = 900;
  17.     private const LOGIN_ROUTES = ['login_user', 'verif_cnx', 'verif_cnx1'];
  18.     private CacheItemPoolInterface $cache;
  19.     public function __construct(CacheItemPoolInterface $cache)
  20.     {
  21.         $this->cache = $cache;
  22.     }
  23.     public static function getSubscribedEvents(): array
  24.     {
  25.         return [KernelEvents::REQUEST => ['onKernelRequest', 8]];
  26.     }
  27.     public function onKernelRequest(RequestEvent $event): void
  28.     {
  29.         if (!$event->isMainRequest()) {
  30.             return;
  31.         }
  32.         $request = $event->getRequest();
  33.         if (!\in_array($request->attributes->get('_route'), self::LOGIN_ROUTES, true)) {
  34.             return;
  35.         }
  36.         $ip = (string) $request->server->get('REMOTE_ADDR', 'inconnue');
  37.         try {
  38.             $item = $this->cache->getItem('login_throttle_' . md5($ip));
  39.             $now = time();
  40.             $data = $item->isHit() ? $item->get() : ['count' => 0, 'start' => $now];
  41.             if (!\is_array($data) || $now - ($data['start'] ?? 0) > self::WINDOW_SECONDS) {
  42.                 $data = ['count' => 0, 'start' => $now];
  43.             }
  44.             $data['count']++;
  45.             $item->set($data)->expiresAfter(self::WINDOW_SECONDS);
  46.             $this->cache->save($item);
  47.         } catch (\Throwable $e) {
  48.             return; // en cas de panne du cache, ne pas bloquer les connexions légitimes
  49.         }
  50.         if ($data['count'] > self::MAX_ATTEMPTS) {
  51.             $reste = max(1, self::WINDOW_SECONDS - (time() - $data['start']));
  52.             $event->setResponse(new Response(
  53.                 'Trop de tentatives de connexion. Réessayez dans quelques minutes.',
  54.                 429,
  55.                 ['Retry-After' => (string) $reste, 'Content-Type' => 'text/plain; charset=UTF-8']
  56.             ));
  57.         }
  58.     }
  59. }