Налаштування 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() при використанні тегованого кешу).







