Охота за IP: почему просто $_SERVER['REMOTE_ADDR'] уже не торт?
Представьте: вы запустили крутой образовательный проект на PHP. Всё летает, пользователи довольны. Но тут вы решаете добавить логирование посетителей, географическую привязку или, скажем, защиту от накрутки голосов. И тут — бац! — выясняется, что половина ваших "уникальных" посетителей сидит с одним и тем же IP: 127.0.0.1 или 10.0.0.1. Знакомо?
Проблема получения реального IP-адреса клиента в PHP — это классика, которая не теряет актуальности. Вопрос с Stack Overflow набирает тысячи просмотров, а новички (и не только) продолжают наступать на одни и те же грабли. В 2025 году, когда Cloudflare, AWS, Nginx и куча CDN-прокси — это стандарт де-факто, полагаться на $_SERVER['REMOTE_ADDR'] — это как гадать на кофейной гуще.
В этой статье мы не просто дадим готовую функцию. Мы разберём анатомию HTTP-заголовков, научимся отличать друга от врага и напишем безопасный и надёжный способ получения IP-адреса посетителя в PHP, который не подведёт вас в 2025 году. Поехали!
REMOTE_ADDR: ваш верный, но слепой пёс
Начнём с базы. $_SERVER['REMOTE_ADDR'] — это IP-адрес того, кто напрямую подключился к вашему веб-серверу. Если ваш сайт висит на выделенном сервере без прокси — это и есть IP клиента. Просто и надёжно.
Но в современном мире так бывает редко. Чаще всего между пользователем и вашим PHP-скриптом стоит целая цепочка:
- CDN (Cloudflare, Fastly)
- Балансировщик нагрузки (Nginx, HAProxy)
- Обратный прокси (Varnish, Nginx)
- Хостинг с общей архитектурой
И вот тут REMOTE_ADDR показывает IP последнего звена (например, вашего балансировщика), а не реального пользователя. Для образовательного сайта это катастрофа: вы не сможете отличить студента из Москвы от студента из Нью-Йорка, а система антиспама будет блокировать всех подряд.
HTTP-заголовки: где прячется настоящий IP?
Чтобы передать реальный IP клиента, прокси-серверы добавляют специальные HTTP-заголовки. Самые популярные в 2025 году:
1. X-Forwarded-For (XFF)
Классический заголовок. Может содержать цепочку IP: client_ip, proxy1_ip, proxy2_ip. Первый IP в списке — это настоящий клиент. Но есть нюанс: этот заголовок может подделать любой злоумышленник, если ваша инфраструктура его не чистит.
2. X-Real-IP
Любимый заголовок Nginx. Содержит только один IP — реальный. Проще и безопаснее, но поддерживается не всеми прокси.
3. CF-Connecting-IP (Cloudflare)
Специфический заголовок Cloudflare. Если вы используете их CDN, это самый надёжный вариант. Но только если вы доверяете Cloudflare (а вы должны, они молодцы).
Важно: Никогда не доверяйте заголовкамX-Forwarded-ForиX-Real-IPот неизвестных источников. Злоумышленник может подставить любой IP, и ваша система будет считать его реальным.
Лучшая практика 2025: многослойная защита
Как же быть? Мы не можем просто брать первый IP из X-Forwarded-For — это опасно. Но и игнорировать прокси нельзя. Решение — белый список доверенных прокси.
Идея проста: мы знаем IP-адреса наших балансировщиков и CDN. Если запрос пришёл от них — мы доверяем заголовкам. Если нет — используем REMOTE_ADDR.
Вот рабочий класс на PHP, который реализует эту логику:
<?php
class ClientIP
{
/**
* Список доверенных IP-адресов и подсетей (CIDR).
* Добавьте сюда IP ваших прокси, CDN, балансировщиков.
*/
private static array $trustedProxies = [
'127.0.0.1', // localhost
'10.0.0.0/8', // приватные сети
'172.16.0.0/12', // приватные сети
'192.168.0.0/16', // приватные сети
'173.245.48.0/20', // Cloudflare (пример)
'103.21.244.0/22', // Cloudflare (пример)
// Добавьте свои IP
];
/**
* Получить реальный IP клиента.
*
* @return string IP-адрес или 'UNKNOWN', если определить не удалось.
*/
public static function get(): string
{
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? 'UNKNOWN';
// Проверяем, пришёл ли запрос от доверенного прокси
if (!self::isTrustedProxy($remoteAddr)) {
return $remoteAddr;
}
// Если прокси доверенный, проверяем заголовки
$headers = [
'HTTP_CF_CONNECTING_IP', // Cloudflare
'HTTP_X_REAL_IP', // Nginx
'HTTP_X_FORWARDED_FOR', // Стандартный X-Forwarded-For
];
foreach ($headers as $header) {
if (!empty($_SERVER[$header])) {
$ip = self::extractClientIp($header, $_SERVER[$header]);
if ($ip && filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
}
return $remoteAddr;
}
/**
* Проверить, является ли IP доверенным прокси.
*/
private static function isTrustedProxy(string $ip): bool
{
foreach (self::$trustedProxies as $proxy) {
if (strpos($proxy, '/') !== false) {
// CIDR проверка
if (self::ipInCidr($ip, $proxy)) {
return true;
}
} else {
// Точное совпадение
if ($ip === $proxy) {
return true;
}
}
}
return false;
}
/**
* Извлечь IP из заголовка X-Forwarded-For (цепочка).
*/
private static function extractClientIp(string $headerName, string $headerValue): ?string
{
// Для CF-Connecting-IP и X-Real-IP — это всегда один IP
if ($headerName !== 'HTTP_X_FORWARDED_FOR') {
return trim($headerValue);
}
// Для X-Forwarded-For берём первый IP из списка
$ips = explode(',', $headerValue);
$firstIp = trim($ips[0]);
return $firstIp ?: null;
}
/**
* Проверить, входит ли IP в CIDR-диапазон.
*/
private