为什么个人站长也需要地域分流
很多站长觉得「按国家/地区分流」是大厂玩的东西——毕竟你的站可能只有中文访客。但实际情况是:只要站点有一定流量,你迟早会遇到这些问题。海外爬虫和扫描器每天成千上万次请求,消耗你的带宽和 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 一层就能搞定的事,不需要动应用代码。