Skip to main content

NGINX 屏蔽部分地区访问,但不完全屏蔽


NGINX 模块

首先我们需要确保你的 NGINX 安装了 GeoIP2 模块,如何安装不再描述。

如果你编译 GeoIP2 模块时出现了找不到 libmaxminddb 的错误,那么需要安装 libmaxminddb-devel 包(RHEL 系列)

安装 libmaxminddb 包后你也能在 shell 中查询,比如:

mmdblookup --file /usr/share/GeoIP/GeoLite2-Country.mmdb --ip 8.8.8.8


GeoIP 库自动更新

我们一般都用 Maxmind 的免费 GeoIP 库 GeoLite2。
你可能想要让你的 IP 库自动更新,可以使用 Maxmind 自家的 geoipupdate

官方文档:https://dev.maxmind.com/geoip/updating-databases

可以安装 Golang 包后直接执行

go install github.com/maxmind/geoipupdate/v6/cmd/geoipupdate@latest
cp /root/go/bin/geoipupdate /usr/local/bin

安装后注册 Maxmind.com 账户,申请免费密钥。
然后在登录状态下访问 https://www.maxmind.com/en/accounts/current/license-key/GeoIP.conf 下载预填充配置文件
将该配置文件移动到 /usr/local/etc/GeoIP.conf 然后把 YOUR_LICENSE_KEY_HERE 改为你申请的密钥。

配置完后只要执行 geoipupdate,IP 数据库就会自动下载到  /usr/local/share/GeoIP
如果下载错误,可以执行 geoipupdate -v 查看详情。

然后就可以用 corntab 自动更新了,比如在 /etc/cron.d 下新建一个 geoipupdate 写入:

29 2 * * 1,3 /usr/local/bin/geoipupdate

即在每周一和每周四的晚上 9 点 50 分执行

NGINX 设置

GeoIP2 设置

http {
    geoip2 /usr/local/share/GeoIP/GeoLite2-Country.mmdb {
        auto_reload 1d;
        #每隔一天自动重载一次 IP 数据库
        $geoip2_data_country_iso_code country iso_code;
        #从 GeoIP2 数据库中提取"country iso_code"字段的值,并将其存储在名为"geoip2_data_country_iso_code"的变量中。
    }
}

默认情况下会使用 $remote_addr 中的地址来进行判定,如果你有 CDN 和直连 的话:

http {
    map $http_x_forwarded_for $real_client_ip {
        # 如果 http_x_forwarded_for 存在,则使用其第一个值(可能包含多个 IP 地址)然后赋值到 real_client_ip 变量。
        "~^(?P<first_ip>[^,]+)" $first_ip;
        # 否则,使用 Nginx 的 remote_addr 变量赋值到 real_client_ip 变量。
        default $remote_addr;
    }


    geoip2 /usr/local/share/GeoIP/GeoLite2-Country.mmdb {
        auto_reload 1d;
        #每隔一天自动重载一次 IP 数据库
        $geoip2_data_country_iso_code_xforward default=unknown source=$real_client_ip country iso_code;
        #如果判断失败,则默认返回 default,来源 IP 设置为 $real_client_ip 变量
        #然后从 GeoIP2 数据库中提取"country iso_code"字段的值,并将其存储在名为"geoip2_data_country_iso_code_xforward"的变量中。
    }
}

国家判断设置

比如想屏蔽 KP(朝鲜) 和 PK(巴基斯坦)

http {
    map "$geoip2_data_country_iso_code_xforward" $block_access {
        "KP" deny; #如果 geoip2_data_country_iso_code_xforward 的值是 KP(朝鲜) 则给 block_access 变量赋值为 deny
        "PK" deny;#如果 geoip2_data_country_iso_code_xforward 的值是 PK(巴基斯坦) 则给 block_access 变量赋值为 deny
        default allow; #默认给 block_access 变量赋值为 allow
    }
}

location 设置

如果你之前是直接用 / 匹配所有的,那么直接在原来的 location 前面加上判断语句即可。

server {
    location / {
        if ($block_access = deny) {
            #如果 block_access 变量是 deny 则返回 451
            return 451;
        }

        #原来的 location 内容
    }
}


不完全屏蔽
http {
    map $http_x_forwarded_for $real_client_ip {
        # 如果 http_x_forwarded_for 存在,则使用其第一个值(可能包含多个 IP 地址)然后赋值到 real_client_ip 变量。
        "~^(?P<first_ip>[^,]+)" $first_ip;
        # 否则,使用 Nginx 的 remote_addr 变量赋值到 real_client_ip 变量。
        default $remote_addr;
    }


    geoip2 /usr/local/share/GeoIP/GeoLite2-Country.mmdb {
        auto_reload 1d;
        #每隔一天自动重载一次 IP 数据库
        $geoip2_data_country_iso_code_xforward default=unknown source=$real_client_ip country iso_code;
        #如果判断失败,则默认返回 default,来源 IP 设置为 $real_client_ip 变量
        #然后从 GeoIP2 数据库中提取"country iso_code"字段的值,并将其存储在名为"geoip2_data_country_iso_code_xforward"的变量中。
    }


    #NGINX 不支持 2 个 IF,使用 map 实现
    map $http_cookie $allow_cookie {
        #如果 cookie 里面有 access_granted=true 那么就把 allow_cookie 变量设置为 true
        default false;
        "~*access_granted=true" true;
    }

    map "$geoip2_data_country_iso_code_xforward" $block_coutry {
        "KP" deny; #如果 geoip2_data_country_iso_code_xforward 的值是 KP(朝鲜) 则给 block_access 变量赋值为 deny
        "PK" deny; #如果 geoip2_data_country_iso_code_xforward 的值是 PK(巴基斯坦) 则给 block_access 变量赋值为 deny
        default allow; #默认给 block_access 变量赋值为 allow
    }


    map "$block_coutry:$allow_cookie" $block_access {
        "deny:false" deny; #如果 IP 是屏蔽国家的而且没有 access_granted=true 的 cookie,那么就拒绝访问
        "deny:true" allow; #如果 IP 是屏蔽国家但有 access_granted=true 的 cookie,那么允许访问
        default allow;
    }
}

location 部分
额外加一个,返回 451 时转跳到 blocked.html。

server {
    location / {
        if ($block_access = deny) {
            #如果 block_access 变量是 deny 则返回 451
            return 451;
        }

        #原来的 location 内容
    }

    error_page 451 /blocked.html;

    location = /blocked.html {
        internal;
}

blocked.html 设置 Cookie 示例,没设置自动刷新,手动刷新即可到正常页面。

<html>
 <head> 
  <meta charset="UTF-8" /> 
  <title>451</title> 
 </head> 
 <body> 
  <div class="access_button" onclick="grantAccess()"> 
   <a href="#">获取访问权限</a> 
  </div> 
  <script>
   function grantAccess() {
       document.cookie = "access_granted=true; path=/";
   }
  </script>  
 </body>
</html>