app/Customize/EventListener/IpRateLimitListener.php line 30

Open in your IDE?
  1. <?php
  2. namespace Customize\EventListener;
  3. use Symfony\Component\HttpKernel\Event\RequestEvent;
  4. use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
  5. use Symfony\Component\HttpKernel\KernelEvents;
  6. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  7. class IpRateLimitListener implements EventSubscriberInterface
  8. {
  9.     private $cacheDir;
  10.     
  11.     public function __construct(string $cacheDir)
  12. {
  13.     $this->cacheDir $cacheDir '/ip_limit';
  14.     if (!is_dir($this->cacheDir)) {
  15.         mkdir($this->cacheDir0777true);
  16.     }
  17. }
  18.     public static function getSubscribedEvents(): array
  19.     {
  20.         return [
  21.             KernelEvents::REQUEST => ['onKernelRequest'15],
  22.         ];
  23.     }
  24.     public function onKernelRequest(RequestEvent $event)
  25.     {
  26.         if (!$event->isMasterRequest()) {
  27.             return;
  28.         }
  29.         $request $event->getRequest();
  30.         
  31.         if (strpos($request->getPathInfo(), '/admin') !== false) {
  32.             return;
  33.         }
  34.         
  35.         if ($request->getMethod() !== 'POST') {
  36.             return;
  37.         }
  38.         
  39.         $ip $request->getClientIp();
  40.         
  41.         // 3段階の制限
  42.         // 1. 短期: 5分間に10回
  43.         $this->checkLimit($ip'short'10300);
  44.         
  45.         // 2. 中期: 1時間に30回
  46.         $this->checkLimit($ip'medium'303600);
  47.         
  48.         // 3. 長期: 24時間に100回(これを超えたら1日ブロック)
  49.         $this->checkLimit($ip'long'10086400);
  50.     }
  51.     
  52.     private function checkLimit($ip$type$maxAttempts$period)
  53.     {
  54.         $key md5($ip '_' $type);
  55.         $file $this->cacheDir '/' $key;
  56.         
  57.         $now time();
  58.         
  59.         if (file_exists($file)) {
  60.             $data json_decode(file_get_contents($file), true);
  61.             
  62.             // 古いデータをクリーンアップ
  63.             $data['attempts'] = array_filter($data['attempts'], function($time) use ($now$period) {
  64.                 return ($now $time) < $period;
  65.             });
  66.             
  67.             if (count($data['attempts']) >= $maxAttempts) {
  68.                 $oldest min($data['attempts']);
  69.                 $waitTime $period - ($now $oldest);
  70.                 
  71.                 // 長期制限に引っかかった場合は特別なログ
  72.                 if ($type === 'long') {
  73.                     error_log(sprintf(
  74.                         '[IP Rate Limit SEVERE] IP: %s blocked for 24 hours. Total attempts: %d',
  75.                         $ipcount($data['attempts'])
  76.                     ));
  77.                     
  78.                     throw new TooManyRequestsHttpException(
  79.                         $waitTime,
  80.                         '送信回数の上限を大幅に超えました。24時間後に再度お試しください。'
  81.                     );
  82.                 }
  83.                 
  84.                 error_log(sprintf(
  85.                     '[IP Rate Limit] IP: %s, Type: %s, Path: %s, Blocked for %d seconds',
  86.                     $ip$type$_SERVER['REQUEST_URI'] ?? 'unknown'$waitTime
  87.                 ));
  88.                 
  89.                 throw new TooManyRequestsHttpException(
  90.                     $waitTime,
  91.                     sprintf('送信回数が多すぎます。%d秒後に再度お試しください。'$waitTime)
  92.                 );
  93.             }
  94.             
  95.             $data['attempts'][] = $now;
  96.         } else {
  97.             $data = [
  98.                 'ip' => $ip,
  99.                 'attempts' => [$now],
  100.                 'first_seen' => date('Y-m-d H:i:s')
  101.             ];
  102.         }
  103.         
  104.         file_put_contents($filejson_encode($data));
  105.     }
  106. }