<?php
namespace Customize\EventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class IpRateLimitListener implements EventSubscriberInterface
{
private $cacheDir;
public function __construct(string $cacheDir)
{
$this->cacheDir = $cacheDir . '/ip_limit';
if (!is_dir($this->cacheDir)) {
mkdir($this->cacheDir, 0777, true);
}
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['onKernelRequest', 15],
];
}
public function onKernelRequest(RequestEvent $event)
{
if (!$event->isMasterRequest()) {
return;
}
$request = $event->getRequest();
if (strpos($request->getPathInfo(), '/admin') !== false) {
return;
}
if ($request->getMethod() !== 'POST') {
return;
}
$ip = $request->getClientIp();
// 3段階の制限
// 1. 短期: 5分間に10回
$this->checkLimit($ip, 'short', 10, 300);
// 2. 中期: 1時間に30回
$this->checkLimit($ip, 'medium', 30, 3600);
// 3. 長期: 24時間に100回(これを超えたら1日ブロック)
$this->checkLimit($ip, 'long', 100, 86400);
}
private function checkLimit($ip, $type, $maxAttempts, $period)
{
$key = md5($ip . '_' . $type);
$file = $this->cacheDir . '/' . $key;
$now = time();
if (file_exists($file)) {
$data = json_decode(file_get_contents($file), true);
// 古いデータをクリーンアップ
$data['attempts'] = array_filter($data['attempts'], function($time) use ($now, $period) {
return ($now - $time) < $period;
});
if (count($data['attempts']) >= $maxAttempts) {
$oldest = min($data['attempts']);
$waitTime = $period - ($now - $oldest);
// 長期制限に引っかかった場合は特別なログ
if ($type === 'long') {
error_log(sprintf(
'[IP Rate Limit SEVERE] IP: %s blocked for 24 hours. Total attempts: %d',
$ip, count($data['attempts'])
));
throw new TooManyRequestsHttpException(
$waitTime,
'送信回数の上限を大幅に超えました。24時間後に再度お試しください。'
);
}
error_log(sprintf(
'[IP Rate Limit] IP: %s, Type: %s, Path: %s, Blocked for %d seconds',
$ip, $type, $_SERVER['REQUEST_URI'] ?? 'unknown', $waitTime
));
throw new TooManyRequestsHttpException(
$waitTime,
sprintf('送信回数が多すぎます。%d秒後に再度お試しください。', $waitTime)
);
}
$data['attempts'][] = $now;
} else {
$data = [
'ip' => $ip,
'attempts' => [$now],
'first_seen' => date('Y-m-d H:i:s')
];
}
file_put_contents($file, json_encode($data));
}
}