Nginx GeoIP2 地域分流实战:按国家放行/拦截/跳转、地域限流与 real_ip 真实 IP 还原

为什么个人站长也需要地域分流

很多站长觉得「按国家/地区分流」是大厂玩的东西——毕竟你的站可能只有中文访客。但实际情况是:只要站点有一定流量,你迟早会遇到这些问题。海外爬虫和扫描器每天成千上万次请求,消耗你的带宽和 CPU;某些国家的恶意流量集中攻击你的登录接口;你想给不同地区的访客展示不同的内容或用不同的 CDN 节点;又或者你要做合规隔离,某些内容只对特定地区开放。

这些需求用 Nginx 的 ngx_http_geoip2_module 都能优雅解决。它的原理很简单:Nginx 加载一份 IP 地理库(最大的是 MaxMind 的 GeoLite2),根据访客 IP 查表得出国家/城市/经纬度,然后你用变量做条件判断——放行、拦截、跳转、限流,全部在 Nginx 层完成,不碰一行业务代码。

本文完整走一遍:编译安装 geoip2 模块、准备 GeoLite2 数据库与自动更新、写配置实现国别分流、按地区限流、以及几个真实的踩坑点。

一、准备 GeoLite2 数据库与 MaxMind 账号

先说明一个现实变化:MaxMind 从 2019 年底起,GeoLite2 数据库不再提供公开直链下载,必须注册免费账号并拿到 License Key,否则你下载到的是空文件或 401 报错。这是最常见的坑,很多人卡在这里。

# 1. 去 maxmind.com 注册免费账号,进入 "Manage License Keys" 生成一个 key
# 2. 用 maxmind 的官方下载工具 geolite2-downloader,或直接用 API

用官方命令行工具最省事(Python 写的,pip 装):

pip install maxminddb-geolite2   # 或以下官方下载器
curl -sSL https://raw.githubusercontent.com/maxmind/geoipupdate/master/geoipupdate.sh

更稳妥的推荐用 MaxMind 官方的 geoipupdate 工具,它支持配置文件和自动更新。先用 apt 装:

apt-get install -y geoipupdate

编辑 /etc/GeoIP.conf:

AccountID 你的账号ID
LicenseKey 你的LicenseKey
EditionIDs GeoLite2-Country GeoLite2-City

执行更新,数据库会下载到 /usr/share/GeoIP/:

geoipupdate -v
ls -lh /usr/share/GeoIP/
# 应能看到 GeoLite2-Country.mmdb 和 GeoLite2-City.mmdb

验证文件不是空壳(这是关键,空文件也会被创建):

ls -l /usr/share/GeoIP/GeoLite2-Country.mmdb
# 正常应有几 MB 大小,若只有几百字节说明下载失败

二、编译安装 ngx_http_geoip2_module

这个模块不是 Nginx 内置的,如果你的 Nginx 是 apt 装的,需要动态模块方式编译。先装依赖并确认 Nginx 版本:

nginx -v          # 记下版本号,例如 nginx/1.24.0
apt-get install -y libmaxminddb-dev build-essential libpcre3-dev zlib1g-dev libssl-dev \
  nginx-dev   # nginx-dev 提供动态模块构建所需的头文件

下载模块源码并编译成动态模块(--add-dynamic-module 参数要求 Nginx 支持动态模块,官方 apt 包都支持):

git clone https://github.com/leev/ngx_http_geoip2_module.git /usr/local/src/geoip2
cd /usr/local/src/geoip2
# 用与你 nginx -v 完全一致的源码版本解压后编译
wget http://nginx.org/download/nginx-1.24.0.tar.gz
tar xzf nginx-1.24.0.tar.gz
cd nginx-1.24.0
./configure --with-compat --add-dynamic-module=/usr/local/src/geoip2
make modules

编译成功后产物是 objs/ngx_http_geoip2_module.so,复制到模块目录并在主配置加载:

mkdir -p /usr/lib/nginx/modules
cp objs/ngx_http_geoip2_module.so /usr/lib/nginx/modules/
echo 'load_module modules/ngx_http_geoip2_module.so;' > /etc/nginx/modules-enabled/50-geoip2.conf
nginx -t

注意:编译时用的 Nginx 源码版本必须和运行版本完全一致,否则加载会报 module is not binary compatible。如果你用 Ubuntu 的 nginx 包,可以用 apt-get source nginx 拿同版本的源码,或直接用 nginx -V 输出里的配置参数来比对。

三、配置国别分流

核心指令是 geoip2,加载数据库并映射到变量。放在 http 块里:

http {
    geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
        auto_reload 5m;                              # 数据库热更新,5分钟检测一次
        $geoip2_metadata_country_build           metadata build_epoch;
        $geoip2_data_country_code                country iso_code;      # 得到 CN / US / JP
        $geoip2_data_country_name                country names en;
        $geoip2_data_country_continent           continent code;        # AS / EU / NA
    }
    # 城市库可选,用于更细粒度
    geoip2 /usr/share/GeoIP/GeoLite2-City.mmdb {
        $geoip2_data_city_name  city names en;
    }
}

$geoip2_data_country_code 就是你后续做判断的主力变量,值形如 CN、US、JP,局域网/未知 IP 会是空字符串。

场景一:屏蔽特定国家的访问

map $geoip2_data_country_code $allowed_country {
    default 0;
    CN 1;
    HK 1;
    TW 1;
    SG 1;
}
server {
    listen 80;
    server_name www.example.com;
    if ($allowed_country = 0) {
        return 403 "Access from your region is not allowed.";
    }
    # ... 正常业务
}

用 map 而不是写一长串 if ($country ~ ^(CN|HK)$),是因为 map 的查找是 O(1) 哈希,且性能更好、可读性更强。注意 if 在 location 里有名的陷阱:上面这个 if 用在 server 级别做 return 是安全的(return 类是 if 的少数安全用法之一),但如果在 location 里用 if 配合 proxy_pass 就可能出问题。做拦截优先用 map + if return。

场景二:按地区跳转到不同站点

map $geoip2_data_country_code $site_variant {
    default   "https://www.example.com";
    JP        "https://jp.example.com";
    US        "https://us.example.com";
}
server {
    listen 80;
    server_name www.example.com;
    # 只在请求首页且非爬虫时跳转,避免影响收录和爬虫抓取
    if ($request_uri = "/" ) {
        set $do_redirect 1;
    }
    if ($http_user_agent ~* "(Googlebot|Bingbot|Baiduspider)") {
        set $do_redirect 0;
    }
    if ($do_redirect = 1) {
        return 302 $site_variant$request_uri;
    }
}

务必排除搜索引擎爬虫,否则 Googlebot 从一个 IP 被跳到美国站、Baiduspider 被跳到中国站,会造成多个 URL 内容和收录混乱。多地区站点还应配套 hreflang 标注,否则 SEO 会打架。

场景三:按地区限流

结合 limit_req 做地域差异化限流。比如对海外(非 CN)请求限得更严,防止海外爬虫刷站:

http {
    map $geoip2_data_country_code $limit_key {
        default  $binary_remote_addr;   # 海外按 IP 限
        CN       "";                    # 国内不限(空 key 表示不限制)
    }
    limit_req_zone $limit_key zone=geo_limit:10m rate=5r/s;

    server {
        listen 80;
        location /api/ {
            limit_req zone=geo_limit burst=10 nodelay;
            limit_req_status 429;
            # ...
        }
    }
}

这里利用了 limit_req 的一个特性:当 key 为空字符串时,该请求不计入限流。所以把国内访客的 key 设为空,等于对国内放行、只限海外。这是一个非常实用的技巧。

四、几个真实的坑

  • GeoLite2 下载不到:必须注册 MaxMind 账号拿 License Key,直连旧链接会 401。下载后一定 ls -l 确认文件大小正常,空文件会导致所有查询返回空值。
  • 模块加载报二进制不兼容:编译用的 Nginx 源码版本和运行版本不一致。用 nginx -V 对齐版本重编。
  • 所有访客都被识别为空 country:多半是数据库路径写错或文件为空;也可能是 Nginx worker 起在 chroot 环境读不到文件。用 error_log 看 MMDB 打开失败日志。
  • CDN 后面的真实 IP 拿不到:如果站套了 Cloudflare 或 CDN,Nginx 看到的是 CDN 机房 IP,geoip2 会一直判定为 CDN 所在国家。必须先用 real_ip 模块还原真实访客 IP,且把 real_ip 配置放在 geoip2 判断之前的处理顺序上——实际做法是 real_ip 改的是 $remote_addr,确保 set_real_ip_from 配置正确后 geoip2 才拿到真 IP。
  • 数据库过期导致判断错误:GeoLite2 每两周更新一次,过期后新分配的 IP 段可能查不到。用 auto_reload 5m + 定时 geoipupdate 保证新鲜。
  • 误伤自己/内网:内网 IP 查询结果为空,会被 map 的 default 命中。测试时把自己常用 IP 加入白名单,或在内网测试环境先验证。
  • IPv6 未覆盖:确保用支持 IPv6 的 MMDB,并确认 Nginx 监听 IPv6,否则 IPv6 访客的 geo 判断会失效。

五、验证与调试

配置完不能凭空相信,用 curl 配合 --resolve 模拟不同地区很难(你没法伪造源 IP),更靠谱的做法是:先在你的响应头里 debug 输出国家码,再找几个真实的不同地区节点(比如用境外 VPS)访问验证。

server {
    location /geo-test {
        add_header X-Debug-Country $geoip2_data_country_code always;
        return 200 "your country: $geoip2_data_country_code\n";
    }
}

然后在不同地区的机器上 curl -I https://你的域名/geo-test,看 X-Debug-Country 是否与预期一致。验证完记得把调试 location 删掉,别把国家码泄露在生产环境。

把地域分流做扎实之后,你会多出很多玩法:按地区做差异化缓存、对高风险地区加强验证码、给海外访客走更近的 CDN 回源、甚至做简单的合规隔离。这些都是 Nginx 一层就能搞定的事,不需要动应用代码。

Last modification:October 13th, 2026 at 10:26 pm

Leave a Comment