以前からCSRFでなりすまし対策の記述、前記事でのreCAPTHAを使ってbot対策は出来たけれど、
人海戦術での嫌がらせの場合は対応できない。
なので、短時間に大量の送信があった場合にIP制限をかけることにしました。

流れとしては
- IPレート制限チェック
- CSRFチェック
- reCAPTCHA検証
- バリデーション
- メール送信
- IPログ保存
- Thanks画面へリダイレクト
の順序で動作させるのがベストかと思うので、1と6を作っていきます。
MySQLでIP管理
まず、ルールとして同一IPから5分以内に3回以上メール送信があった場合ブロックすることとします。
また、レンタルサーバーでも確実に動くため、MySQLを使ってIP管理をしようと思います。
CREATE TABLE contact_rate_limits (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
ip_address VARCHAR(45) NOT NULL,
created_at DATETIME NOT NULL,
INDEX idx_ip_time (ip_address, created_at)
);IPv6対応のため ip_address は VARCHAR(45)にしておきました。
また、時間検索用に index を貼ります。
IPレート制限チェック
Controllerの送信処理の一番上に下記を記述します。
特にreCAPTCHAはGoogleとのAPI通信をしているため、
無駄な API 通信を防ぐためにも一番上に記述するのが適当かと思います。
$ip = $_SERVER['REMOTE_ADDR'];
// 直近5分の送信回数を取得
$stmt = $pdo->prepare(
'SELECT COUNT(*) FROM contact_rate_limits
WHERE ip_address = ?
AND created_at >= (NOW() - INTERVAL 5 MINUTE)'
);
$stmt->execute([$ip]);
$count = (int)$stmt->fetchColumn();
if ($count >= 3) {
exit('短時間に送信が集中しています。しばらく時間を置いてからお試しください。');
}REMOTE_ADDR というのがアクセス元のIPアドレスになります。
IPログ保存
メール送信が成功した後に下記を記述します。
$stmt = $pdo->prepare(
'INSERT INTO contact_rate_limits (ip_address, created_at)
VALUES (?, NOW())'
);
$stmt->execute([$ip]);DB接続に嵌まる
何故か Undefined variable $pdo が出てしまう。
それじゃあDB操作なんて何もできないよ。
調べたら、管理画面で流用していたDB接続用のPHPファイルがうまく読み込めていなかった。
それを読み込むだけじゃダメだったので、$pdo自体を返すように改変した。
<?php
$host = 'localhost:ポート番号';
$db = 'DB名';
$user = 'ユーザー名';
$pass = 'パスワード';
$dsn = "mysql:host=$host;dbname=$db;charset=utf8mb4";
try {
return $pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
} catch (Exception $e) {
echo 'DB接続エラー: ', $e->getMessage();
exit;
}
?>$pdo の前に return を書くだけでOK!
また、Controllerには下記を記述してグローバル変数を減らした。
$pdo = require __DIR__ . '/../../public/config.php';
無事に書き込まれました!(なんかIPアドレスおかしいけど)
定期的にDBに書き込まれたIPアドレスを削除
アクセスされる度にログはどんどん溜まっていくので、定期的に削除処理が必要になります。
ということで、1日以上経ったログは削除する方法を追記します。
削除用のPHPファイルの作成
<?php
$pdo = require __DIR__ . '/../../public/config.php';
// 1日以上前のログを削除
$stmt = $pdo->prepare(
'DELETE FROM contact_rate_limits
WHERE created_at < (NOW() - INTERVAL 1 DAY)'
);
$deleted = $stmt->execute();
// cronログ用
echo date('Y-m-d H:i:s') . " cleanup done\n";
?>とりあえずこれでよし。
タスクスケジューラに登録
筆者はWindows11ユーザーなので、その方法をご紹介します。
まずスタートボタンから「タスクスケジューラ」と入力して、アプリを表示します。

カテゴリを「すべて」にすると、ブラウザで検索されてしまうことがあるので、
「アプリ」ボタンを押下することをお勧めします。

タスクスケジューラが表示されたら、右上の基本タスクの作成を押下します。
トリガーは下記のように設定しました。
- 毎日
- 間隔:1日
- プログラムの開始
プログラムには下記を設定します。
- (php.exeがあるフォルダ)/php.exe
続いて、引数には下記を設定します。
- (PHPフォルダ)/Jobs/cleanup_contact_rate_limits.php
どちらも絶対パスで入力してください。
「開始」は省略して大丈夫です。
DB自体にイベントスケジューラを登録する場合
DB自体にイベントスケジューラを登録することもできます(これの方が現実的かも)

DBをクリック後、右上の方に イベントボタン が表示されるので、それをクリック。

表示されたフォームに下記を記述。
- イベント名:お好きな名前で
- 状態:ENABLED
- イベント種別:RECURRING
- 実行間隔:1 DAY
- 開始、終了は空白でOK
- 定義:前述した下記のSQL
DELETE FROM contact_rate_limits
WHERE created_at < (NOW() - INTERVAL 1 DAY)これを設定すれば、メールが送られた1日後にはレコードが削除されます。
