Настройка Geo-IP определения региона пользователя на сайте
Определение региона по IP — фундамент для персонализации контента, регионального ценообразования и аналитики. Задача выглядит простой, но в деталях много нюансов: точность баз данных, IPv6, прокси и VPN, кэширование, обновление данных.
Выбор базы данных GeoIP
MaxMind GeoLite2 — бесплатная база, требует регистрации и лицензионного ключа. Обновляется каждый вторник. Точность по городам для России — около 80%, по странам — 98%+.
MaxMind GeoIP2 City — платная версия с повышенной точностью и включёнными ISP-данными. Оправдана для e-commerce с региональным ценообразованием.
ip-api.com / ipinfo.io — HTTP API, без локальной базы. Подходит для небольшой нагрузки (до нескольких тысяч запросов в день). Добавляет задержку ~50–200 мс на каждый первый визит.
Для продакшн-сайта с любой значимой нагрузкой — только локальная база MaxMind.
Установка MaxMind GeoLite2
# Установка geoipupdate для автоматических обновлений базы
apt-get install geoipupdate
# /etc/GeoIP.conf
AccountID 123456
LicenseKey ваш_ключ_из_личного_кабинета
EditionIDs GeoLite2-City GeoLite2-Country
# Первичная загрузка
geoipupdate
# Cron: каждую среду в 3:00 (база обновляется по вторникам)
0 3 * * 3 /usr/bin/geoipupdate
База сохраняется в /var/lib/GeoIP/GeoLite2-City.mmdb.
PHP: пакет geoip2/geoip2
composer require geoip2/geoip2
// app/Services/GeoIpService.php
use GeoIp2\Database\Reader;
use GeoIp2\Exception\AddressNotFoundException;
class GeoIpService
{
private Reader $reader;
public function __construct()
{
$this->reader = new Reader(config('geoip.database_path'));
}
public function lookup(string $ip): array
{
// Не ищем по RFC1918 адресам — это интранет
if ($this->isPrivateIp($ip)) {
return $this->defaultResult();
}
try {
$record = $this->reader->city($ip);
return [
'country_code' => $record->country->isoCode, // 'RU'
'country_name' => $record->country->name, // 'Russia'
'region_code' => $record->subdivisions[0]?->isoCode, // 'MOW'
'region_name' => $record->subdivisions[0]?->name, // 'Moscow'
'city' => $record->city->name, // 'Moscow'
'latitude' => $record->location->latitude,
'longitude' => $record->location->longitude,
'timezone' => $record->location->timeZone, // 'Europe/Moscow'
'is_vpn' => false, // GeoLite2 не определяет VPN
];
} catch (AddressNotFoundException) {
return $this->defaultResult();
}
}
private function isPrivateIp(string $ip): bool
{
return filter_var($ip, FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false;
}
private function defaultResult(): array
{
return [
'country_code' => config('geoip.default_country'),
'region_name' => null,
'city' => null,
'timezone' => config('app.timezone'),
];
}
}
Получение реального IP за прокси/балансировщиком
Nginx и облачные балансировщики передают оригинальный IP через заголовки. Важно доверять только известным IP балансировщиков:
// config/trustedproxies.php или bootstrap/app.php (Laravel 11)
->withMiddleware(function (Middleware $middleware) {
$middleware->trustProxies(
proxies: ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'],
headers: Request::HEADER_X_FORWARDED_FOR
);
})
Cloudflare передаёт реальный IP в заголовке CF-Connecting-IP — его нужно обработать отдельно:
public function getClientIp(Request $request): string
{
// Cloudflare
if ($cf = $request->header('CF-Connecting-IP')) {
return $cf;
}
return $request->ip();
}
Кэширование результатов
Обращение к базе MMDB быстрое (~0.1 мс), но для высоконагруженных сайтов кэшируем в Redis по IP:
public function lookupCached(string $ip): array
{
return Cache::remember(
"geoip:{$ip}",
86400, // 24 часа
fn() => $this->lookup($ip)
);
}
Для приложений с тысячами уникальных IP в час — кэш Redis с TTL 24 ч даст ощутимый прирост. Для обычных сайтов — достаточно просто не делать lookup при каждом запросе одного пользователя (хранить в сессии).
Хранение в сессии
// app/Http/Middleware/DetectUserRegion.php
public function handle(Request $request, Closure $next): Response
{
if (!$request->session()->has('geo')) {
$ip = app(GeoIpService::class)->getClientIp($request);
$geo = app(GeoIpService::class)->lookupCached($ip);
$request->session()->put('geo', $geo);
}
View::share('userGeo', $request->session()->get('geo'));
return $next($request);
}
После первого запроса данные лежат в сессии — база GeoIP не опрашивается снова.
IPv6
MaxMind GeoLite2 поддерживает IPv6. Единственный нюанс — PHP-функция filter_var корректно обрабатывает IPv6, но isPrivateIp нужно дополнить диапазонами:
private function isPrivateIp(string $ip): bool
{
// Локальные IPv6 адреса
if (str_starts_with($ip, '::1') || str_starts_with($ip, 'fc') || str_starts_with($ip, 'fd')) {
return true;
}
return filter_var($ip, FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false;
}
Точность и ограничения
GeoLite2 определяет город в ~75–85% случаев для пользователей из России. Для крупных городов точность выше. Пользователи через VPN или корпоративные прокси будут определены по IP выходного узла — не по реальному местоположению.
Для критичных сценариев (например, показ региональных цен) стоит предусмотреть ручной выбор региона с сохранением в куку — пользователь всегда может скорректировать определение. Это одновременно решает проблему VPN-пользователей и соответствует ожиданиям аудитории.
Обновление базы
База GeoLite2 обновляется по вторникам. geoipupdate в cron обеспечивает актуальность без ручного вмешательства. При обновлении базы рекомендуется сбросить Redis-кэш GeoIP (Cache::tags(['geoip'])->flush() при использовании тегированного кэша).







