Вентиляция и электрика · 12 мин
Как сделать бан по IP на PHP
Пошаговая инструкция по блокировке IP-адресов на PHP. Примеры кода, защита от обхода, работа с .htaccess и базами данных.
Блокировка нежелательных посетителей по IP-адресу — одна из базовых задач обеспечения безопасности веб-приложений. На языке PHP реализовать эту функцию можно несколькими способами: от простого сравнения строк до интеграции с базами данных и серверными конфигурациями. Выбор конкретного метода зависит от нагрузки на сервер, количества записей в черном списке и требований к производительности.
Чаще всего необходимость в бане возникает при защите от брутфорс-атак, спама в комментариях или парсинга контента ботами. Важно понимать, что IP-адрес пользователя не является статичным гарантом его личности: провайдеры часто выдают динамические адреса, а злоумышленники могут использовать прокси или сети Tor. Поэтому механизм блокировки должен быть гибким, а список запрещенных адресов — регулярно обновляться.
В этой статье мы рассмотрим практические аспекты реализации бана по IP средствами PHP. Мы разберем создание класса-защитника, работу с файлами конфигурации, использование баз данных для масштабных списков и интеграцию с веб-сервером для повышения эффективности. Особое внимание уделим вопросам безопасности, чтобы ваш скрипт блокировки сам не стал уязвимостью.
Простая реализация через массив и файлы
Самый доступный метод для небольших сайтов — хранение списка запрещенных адресов в текстовом файле или PHP-массиве. Этот подход не требует настройки базы данных и легко внедряется в existing проекты. Суть метода заключается в том, что скрипт при каждом запросе считывает список и сравнивает текущий IP посетителя с записями в списке.
Для начала необходимо получить реальный IP-адрес пользователя. В PHP это делается через суперглобальный массив `$_SERVER`. Однако полагаться только на `REMOTE_ADDR` нельзя, так как за балансировщиком нагрузки или прокси реальным адресом будет адрес шлюза. Нужно проверять заголовки `HTTP_X_FORWARDED_FOR` или `HTTP_CLIENT_IP`, но делать это с осторожностью, так как их можно подделать.
```php
function getUserIP() {
$ip = '';
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
$ip = $_SERVER['REMOTE_ADDR'];
}
// Валидация формата IP (IPv4)
return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : '0.0.0.0';
}
```
После получения адреса происходит проверка. Список запрещенных IP можно хранить в файле `banned.txt`, где каждый адрес записан с новой строки. Скрипт читает файл в массив и использует функцию `in_array` для поиска совпадений. Если совпадение найдено, скрипт прекращает выполнение и выдает ошибку 403 Forbidden.
```php
$bannedIPs = file('banned.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$userIP = getUserIP();
if (in_array($userIP, $bannedIPs)) {
http_response_code(403);
die('Доступ запрещен.');
}
```
Этот метод хорош своей простотой, но имеет ограничения по производительности. При каждом запросе происходит чтение файла с диска. Если список содержит тысячи записей, операция поиска линейным перебором станет узким местом. Кроме того, частое чтение файла может создавать нагрузку на дисковую подсистему при высоком трафике.
Работа с масками подсетей и диапазонами
Злоумышленники редко используют один статический IP. Чаще они меняют адреса в пределах одной подсети, выделенной провайдером. Блокировка каждого адреса по отдельности неэффективна. Гораздо разумнее реализовать поддержку масок подсетей (CIDR) или диапазонов IP. Это позволяет одной записью закрыть доступ для целой группы адресов.
Реализация проверки по маске требует перевода IP-адреса в числовое представление. В PHP для этого существует функция `ip2long`. Она преобразует строковый формат IPv4 в целое число. Аналогично преобразуется маска сети. Сравнение происходит путем побитового И (AND) между IP пользователя и маской подсети.
```php
function checkCIDR($ip, $cidr) {
list($subnet, $mask) = explode('/', $cidr);
$ipLong = ip2long($ip);
$subnetLong = ip2long($subnet);
$maskLong = -1 << (32 - $mask);
return ($ipLong & $maskLong) === ($subnetLong & $maskLong);
}
```
В массив запрещенных адресов теперь можно добавлять записи вида `192.168.1.0/24`. Скрипт должен перебирать не только точные совпадения, но и проверять каждую запись на принадлежность к подсети. Это усложняет логику проверки, но значительно повышает эффективность защиты от распределенных атак с одного источника.
Важно учитывать, что функция `ip2long` возвращает знаковое целое число. На 32-битных системах это может привести к отрицательным значениям для адресов выше 127.x.x.x. Для корректной работы рекомендуется использовать беззнаковые сравнения или приводить результаты к строке через `sprintf('%u', ...)`, чтобы избежать ошибок интерпретации чисел.
Интеграция с базой данных для масштабируемости
Когда количество заблокированных адресов исчисляется тысячами или требуется динамическое управление баном через админ-панель, текстовые файлы становятся непрактичными. В этом случае необходимо использовать базу данных (MySQL, PostgreSQL). Хранение данных в БД позволяет использовать индексы для ускорения поиска и управлять списком без редактирования файлов кода.
Структура таблицы должна быть максимально простой для быстродействия. Достаточно двух полей: `ip_address` (VARCHAR или INT) и `expire_time` (для временных банов). Индекс по полю адреса обязателен. При проверке пользователя выполняется SQL-запрос `SELECT` с условием совпадения IP. Если запись найдена и время действия не истекло, доступ блокируется.
CREATE TABLE ip_bans (
id INT AUTO_INCREMENT PRIMARY KEY,
ip_address VARCHAR(45) NOT NULL,
reason VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX (ip_address)
);Для оптимизации производительности не стоит делать запрос к базе при каждом хите страницы, если это высоконагруженный проект. Лучше использовать кэширование. Скрипт может раз в минуту загружать актуальный список банов из БД в Redis или Memcached, а проверку производить уже по кэшу. Это снимает нагрузку с базы данных и ускоряет ответ сервера.
Временные баны реализуются через проверку поля `expire_time`. При каждом запросе скрипт сравнивает текущее время с временем истечения бана. Если бан истек, запись может быть автоматически удалена или просто проигнорирована. Это удобно для блокировки за спам или агрессивное поведение, когда нет смысла закрывать доступ навсегда.
Блокировка на уровне веб-сервера
Самый производительный способ блокировки — перенести эту задачу с уровня PHP на уровень веб-сервера (Apache или Nginx). Когда запрос блокируется в PHP, сервер все равно тратит ресурсы на запуск интерпретатора, загрузку ядра CMS и подключение к БД. Веб-сервер же может отклонить соединение до запуска любых скриптов, экономя до 90% ресурсов процессора.
В Apache это реализуется через файл `.htaccess` с директивами `Deny` или `Require`. Вы можете программно обновлять этот файл из PHP при обнаружении атаки. Однако частая перезапись `.htaccess` может быть опасной: синтаксическая ошибка в файле приведет к ошибке 500 на всем сайте. Используйте атомарную запись через временный файл и переименование.
Для Nginx конфигурация хранится в основных файлах конфига, которые требуют перезагрузки сервиса при изменении. Прямое редактирование из PHP здесь сложнее. Оптимальный вариант — использовать модуль `ngx_http_limit_req_module` для защиты от флуда или выносить список IP в отдельный файл `include`, который можно обновлять без полного рестарта, если конфигурация сервера это позволяет.
Комбинированный подход часто является лучшим решением. Критические атаки (DDoS, сканеры уязвимостей) отбиваются на уровне Nginx/Apache. Логические нарушения (спам в форме, неверные пароли) обрабатываются на уровне PHP, так как требуют анализа контента запроса, а не только заголовков соединения.
Защита от подмены IP и прокси
Одной из главных уязвимостей самодельных скриптов бана является доверие к заголовкам `X-Forwarded-For`. Злоумышленник может легко подделать этот заголовок в своем запросе. Если ваш скрипт берет IP из этого заголовка, атакующий может указать там IP-адрес администратора сайта, тем самым заблокировав владельца ресурса (Self-DoS атака).
Никогда не используйте заголовки `X-Forwarded-For` или `Client-IP` как единственный источник истины, если вы не находитесь за доверенным прокси (например, Cloudflare или балансировщиком нагрузки внутри закрытого контура). Если прокси нет, единственно верным источником является `REMOTE_ADDR`. Заголовки можно использовать только как дополнительную информацию для логирования.
Если ваш сайт работает за Cloudflare, реальный IP посетителя скрыт. В этом случае нужно проверять специальные заголовки, которые добавляет Cloudflare (например, `CF-Connecting-IP`), но только после проверки того, что запрос действительно пришел с IP-адресов Cloudflare. В противном случае любой пользователь сможет притвориться сервисом и скрыть свой настоящий адрес.
Также стоит учитывать IPv6. Адреса формата IPv6 значительно длиннее и имеют другую структуру. Функции работы с IPv4 (`ip2long`) не подойдут. Для поддержки современного интернета необходимо использовать функции `inet_pton` и `inet_ntop`, которые корректно обрабатывают оба формата адресов, или библиотеки вроде `ip-anonymizer`.
Логирование и управление черным списком
Эффективная система бана невозможна без логирования. Вы должны знать, кто, когда и за что был заблокирован. Создайте отдельную таблицу или файл логов, куда записывается IP, время попытки доступа, URL, к которому обращались, и User-Agent. Это поможет анализировать атаки и отличать реальных злоумышленников от случайных совпадений.
Управление списком должно быть удобным. В идеале нужна админ-панель, где можно вручную добавить IP, установить время бана и причину. Автоматический бан (например, после 5 неверных попыток входа) должен иметь кнопку "Разбанить" на случай ошибки. Всегда оставляйте себе "черный ход" — возможность зайти на сайт с определенного доверенного IP даже при включенном режиме полной блокировки.
Периодически проводите чистку списка. IP-адреса, особенно динамические, со временем могут быть выданы другим пользователям. Вечный бан по динамическому IP может наказывать невиновных людей спустя месяцы. Реализуйте скрипт, который раз в месяц удаляет записи о банах старше определенного срока, если они не помечены как "постоянные".
Как выбрать действие
Помните о юридических аспектах. В некоторых юрисдикциях сбор и обработка IP-адресов считается обработкой персональных данных. Убедитесь, что ваша политика конфиденциальности описывает использование IP для безопасности, и не храните логи дольше необходимого срока.
Как разбанить себя, если скрипт заблокировал мой IP?
Если у вас есть доступ к файловой системе хостинга (FTP/SSH), найдите файл со списком банов (например, banned.txt или конфиг БД) и удалите свою запись вручную. Если доступ заблокирован полностью, обратитесь в техническую поддержку хостинга — они могут временно отключить модуль безопасности или изменить IP вашего подключения.
Работает ли бан по IP против пользователей с динамическим адресом?
Да, работает, но с ограничениями. Если провайдер сменит пользователю IP-адрес (переподключит роутер или по таймеру), блокировка перестанет действовать, так как новый адрес не будет в списке. Для таких случаев эффективнее использовать бан по сессиям, кукам или отпечатку браузера в связке с IP.
Можно ли заблокировать целую страну на PHP?
Технически можно, используя GeoIP базы данных (например, MaxMind). Скрипт определяет страну по IP и блокирует доступ, если страна не в списке разрешенных. Однако это сильно нагружает сервер. Для блокировки стран лучше использовать модули веб-сервера или CDN (например, правила в Cloudflare), так как они берут эту нагрузку на себя.
Влияет ли использование VPN на эффективность бана?
Пользователи VPN постоянно меняют IP-адреса. Забанить одного конкретного юзера с VPN сложно, придется блокировать целые подсети VPN-провайдеров, что заденет и честных пользователей. В таких случаях эффективнее капча или поведенческий анализ, чем простая IP-блокировка.
Нужно ли экранировать IP перед записью в базу данных?
IP-адреса состоят из цифр и точек, они не содержат SQL-специфичных символов, опасных для инъекций в классическом понимании. Однако, если вы принимаете IP от пользователя (что само по себе рискованно) и вставляете в запрос, используйте подготовленные выражения (Prepared Statements) в PDO или mysqli для гарантированной безопасности.
Как проверить, заблокирован ли я, не видя кода сайта?
Попробуйте зайти на сайт с мобильного интернета (отключив Wi-Fi), чтобы сменить IP. Если с мобильного сайт работает, а с домашнего нет — вероятно, ваш домашний IP в черном списке. Также можно использовать онлайн-сервисы для проверки доступности сайта из разных стран.