Настройка Geo-IP определения региона пользователя на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Настройка Geo-IP определения региона пользователя на сайте
Средняя
от 1 рабочего дня до 3 рабочих дней
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Настройка 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() при использовании тегированного кэша).