<?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;
// ホワイトリストIP(自分のIPアドレスを追加)
private $whitelist = [
'127.0.0.1',
'::1',
// 自分のIPアドレスをここに追加
// 例: '123.456.789.012',
];
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();
$ip = $request->getClientIp();
// ホワイトリストIPは除外
if (in_array($ip, $this->whitelist)) {
return;
}
// 管理画面は除外
if (strpos($request->getPathInfo(), '/admin') === 0) {
return;
}
// ログインセッションチェック
$session = $request->getSession();
if ($session) {
// 管理者ログイン中は除外
if ($session->has('_security_admin')) {
return;
}
// 顧客ログイン中も除外(オプション)
// if ($session->has('_security_customer')) {
// return;
// }
}
// POSTリクエストのみチェック
if ($request->getMethod() !== 'POST') {
return;
}
// 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));
}
}