在日常工作中有时候我们需要获取用户所在的地区以便做一些特殊处理,比如根据地区展示不同的内容,或者限制某些地区的访问等。本文将介绍如何使用 Nginx + GeoIP2 模块获取用户所在地区以及限制指定地区访问。

准备工作

首先需要安装 ngx_http_geoip2_module 模块,注意和官方文档中的 ngx_http_geoip_module 模块是不一样的,区别在于后者不支持新的 GeoIP2 数据库。文档中列出了详细的编译步骤这里就不赘述,编译和运行都需要 libmaxminddb 库;也可以使用 nginx-geoip2 镜像 进行容器化部署,该镜像支持在stream 指令中使用 geoip2。

安装好 ngx_http_geoip2_module 模块后还需要下载 GeoIP2 数据库,注册好账号后即可免费下载,注意要选择 mmdb 格式的数据库,如需获取客户端的城市信息要选择 City 数据库下载:

20240424103913
20240424103913

嫌麻烦的同学可以从 镜像站 下载

下载好数据库后解压得到 GeoLite2-City.mmdb 文件准备工作就结束了,接下来我们来看如何使用。

配置 Nginx

ngx_http_geoip2_module 模块的配置非常简单,基本上只需要用到一条指令:

1
2
3
4
5
6
7
8
load_module modules/ngx_http_geoip2_module.so; # <- 加载模块

http {
geoip2 <db_path> {
<$variable_name> default=<default_value> source=<$variable> <path>;
// ...
}
}

解释一下 geoip2 指令的参数:

  • db_path 是 GeoIP2 数据库的路径,可以是绝对路径也可以是相对路径,如果是相对路径则相对于 Nginx 的安装目录;
  • variable_name 是自定义变量名,用于存储获取到的地理位置信息,比如 geoip2_city_name geoip2_country_name 等;
  • default_value 当获取不到地理位置信息时的默认值;
  • source 是 IP 地址的来源,默认使用内置的 $remote_addr 变量,如果 Nginx 前面还有负载均衡或者网关,可以根据实际情况使用 $http_x_forwarded_for $http_x_real_ip 等变量;
  • path 是 GeoIP2 数据库中的 字段 路径,可以使用 mmdblookup 工具查看;

下面来实际操作一下,运行一个简单的 Nginx 服务,返回客户端的 IP 地址和地理位置信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
load_module modules/ngx_http_geoip2_module.so; # <- 加载模块

http {
geoip2 /usr/share/GeoIP/GeoLite2-City.mmdb {
$geoip2_country_name default=unknown source=$http_x_real_ip country iso_code;
$geoip2_province_name default=unknown source=$http_x_real_ip subdivisions 0 iso_code;
$geoip2_city_name default=unknown source=$http_x_real_ip city names en;
}

server {
listen 80;
location / {
return 200 "Address=$http_x_real_ip Country=$geoip2_country_name Province=$geoip2_province_name City=$geoip2_city_name";
}
}
}

上面的配置中我们定义了三个变量 $geoip2_country_name $geoip2_province_name $geoip2_city_name 分别用于存储客户端的国家、省份和城市信息,如果获取不到信息则默认值为 unknown,IP 地址的来源是 $http_x_real_ip;字段路径分别是 country iso_code subdivisions 0 iso_code city names en,大多数情况下路径无需修改,但如果需要获取更多信息可以借助 mmdblookup 工具查看,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ mmdblookup --file GeoLite2-City.mmdb --ip 119.23.145.22
{
"city":
{
"geoname_id":
1795565 <uint32>
"names":
{
"de":
"Shenzhen" <utf8_string>
"en":
"Shenzhen" <utf8_string>
"es":
"Shenzhen" <utf8_string>
"fr":
"Shenzhen" <utf8_string>
"ja":
"深セン市" <utf8_string>
"pt-BR":
"Shenzhen" <utf8_string>
"ru":
"Шэньчжэнь" <utf8_string>
"zh-CN":
"深圳" <utf8_string>
}
}
...
}

mmdblookup 会返回类似 JSON 格式的数据,字段路径就是 JSON 的路径,数组字段直接用下标索引即可。

下面来访问测试一下:

1
2
3
$ curl http://127.0.0.1 -H 'X-Real-IP:119.23.145.226'

Address=119.23.145.226 Country=CN Province=GD City=Shenzhen

可以看到 Nginx 正确返回了客户端的 IP 地址和地理位置信息,下面来看几个典型的应用场景:

  • 根据地区展示不同的内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
http {
map $geoip2_country_name $website_language {
CN zh-CN;
JP ja;
default en;
}

server {
listen 80;

location / {
root /www/$website_language;
index index.html;
}
}
}
  • 将客户端的 IP 地址和地理位置信息传递给上游服务:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
http {
server {
listen 80;

location / {
proxy_pass http://upstream;
proxy_set_header X-Real-IP $http_x_real_ip;
proxy_set_header X-Country-Name $geoip2_country_name;
proxy_set_header X-Province-Name $geoip2_province_name;
proxy_set_header X-City-Name $geoip2_city_name;
}

location /php {
fastcgi_pass php:9000;
fastcgi_param REMOTE_ADDR $http_x_real_ip;
fastcgi_param GEOIP_COUNTRY_NAME $geoip2_country_name;
fastcgi_param GEOIP_PROVINCE_NAME $geoip2_province_name;
fastcgi_param GEOIP_CITY_NAME $geoip2_city_name;
}
}
}
  • 限制指定地区访问:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
http {
map $geoip2_country_name $is_allowed {
CN 1;
default 0;
}

server {
listen 80;

location / {
if ($is_allowed = 0) {
return 403 "Forbidden";
}

return 200 "Welcome!";
}
}
}
  • 限制指定地区访问 TCP 服务:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
load_module modules/ngx_stream_geoip2_module.so; # <-- 加载模块

stream {
geoip2 /usr/share/GeoIP/GeoLite2-City.mmdb {
$geoip2_country_name default=unknown source=$remote_addr country iso_code; # <-- 只能从 $remote_addr 获取客户端 IP 地址
}

map $geoip2_country_name $backend_server {
CN "mysql-server:3306";
default "127.0.0.1:12345"; # <-- 拒绝访问
}

server {
listen 3306;
proxy_pass $backend_server; # <-- stream block 中无法使用 if 指令
}

server {
listen 12345;
return ""; # <-- 返回内容并关闭 TCP 连接
}
}