一、问题背景二、聊一嘴ip库三、解决方案四、下载安装openresty五、验证效果六、geoIp自动更新七、参考
在一些中小型项目,会使用nginx作为流量和业务网关,实现流量分发、代理穿透以及负载等能力,当然也可以做一些流量管控和ip过滤限制等能力。
有些出海业务,其相关产品能力和业务接口只对某些国家ip开放,那么我们本着在离用户最近的位置过滤和防控原则,考虑在nginx做一些事情来实现ip识别和限制。
全球范围内,每天ip都在增长,但是不同的区域、不同的国家的ip增长,理论上要满足一定的规则,但是实际增长规则和期望通常会发生违背,所有目前来说暂时没有找到一统江湖并且和实际情况相匹配的规则。
退而言之,现在有很多组织都在维护ip库来做ip定位和过滤的事情,当然也有很多开源组织在做,目前来看市面上主流的如下:
官网:http://www.ipip.net/
优势:支持离线下载,数据更新频繁,数据精确
劣势:收费
官网:http://dev.maxmind.com/zh-hans/geoip/legacy/geolite/
优势:支持离线下载,数据如果是收费版的更新频繁,数据精确,有大致经纬度
劣势:免费版更新不频繁,大概每个月更新一次
优势:免费,支持离线下载,数据显示到省市
劣势:数据更新不频繁(有些地址查不到)
官网:http://qqzeng.com/
优势:支持离线下载,数据更新频繁,数据精确
劣势: 收费
优势:免费,性能好
劣势: 数据更新不频繁,频率为止
geoIp免费版基本够用。
综合考虑技术现状和业务诉求,解决方案理应遵守以下原则:
所以我们一期初步选定基于geoIp免费版来实现:
名称 | 每秒ip查询速度 | 命中率 | 更新频率 |
---|---|---|---|
geoIp(免费版) | 1.8039w/s | 99.65% | 每月更新 |
geoIp免费版支持的能力和特性,基本能够满足我们的业务诉求。
目前我们每个app都有一个nginx网关,那么基于 在离用户最近的位置做过滤 原则,我们可以考虑使用nginx + geoIp来做ip过滤。
注意:5和6属于核心规则配置,并且有执行优先级,如果ip在白名单,直接放过,如果不在白名单则执行geoIp过滤规则。
mkdir -p /opt/tools/openresty
wget https://openresty.org/download/openresty-1.22.4.1.tar.gz
tar -xzvf openresty-1.21.4.1.tar.gz
2.编译安装
./configure --with-compat --with-debug --with-file-aio --with-google_perftools_module --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_degradation_module --with-http_flv_module --with-http_geoip_module=dynamic --with-stream_geoip_module=dynamic --with-http_gunzip_module --with-http_gzip_static_module --with-http_image_filter_module=dynamic --with-http_mp4_module --with-http_perl_module=dynamic --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-http_xslt_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-threads
报错:
把所有依赖包都安装:
yum install gcc
yum -y install pcre pcre-devel openssl openssl-devel
yum -y install libxml2-devel libxslt-devel
yum -y install gd gd-devel
yum -y install perl-ExtUtils-Embed
yum -y install GeoIP-devel
yum -y install libunwind-devel google-perftools-develit
yum -y install gperftools
再执行./configure命令,没有报错。
编译安装:
在/opt/tools/openresty/openresty-1.21.4.1目录执行gmake
执行gmake install
gmake install
可以看到openresty已经安装到了/usr/local/openresty/nginx/sbin/nginx目录,并且软链到/usr/local/openresty/bin/openresty路径。
检查openresty是否安装成功:
cd /usr/local/openresty/nginx/sbin
nginx -V
可以看到nginx的版本已经显示为nginx version: openresty/1.21.4.1.
启动openresty:
/usr/local/openresty/nginx/sbin/nginx
到这里openresty安装并启动成功了。
在配置文件server模块添加:
location ^~ /lua/hello{
default_type 'text/plain';
content_by_lua 'ngx.say("Hello, Lua!")';
}
发送请求:
curl http://localhost:80/lua/hello
openresty默认已经支持了lua和redis的集成,直接编写lua脚本:
local redis_host = "host"
local redis_port = 6379
local redis_connection_timeout = 100
local redis_key = "ip_whitelist_key"
local cache_ttl = 1
local ip = ngx.var.remote_addr
local ip_whitelist = ngx.shared.ip_whitelist
local last_update_time = ip_whitelist:get("last_update_time");
if last_update_time == nil or last_update_time < ( ngx.now() - cache_ttl ) then
local redis = require "resty.redis";
local red = redis:new();
red:set_timeout(redis_connect_timeout);
local ok, err = red:connect(redis_host, redis_port);
if not ok then
ngx.log(ngx.ERR, "Redis connection error while retrieving ip_whitelist: " .. err);
else
local new_ip_whitelist, err = red:smembers(redis_key);
if err then
ngx.log(ngx.ERR, "Redis read error while retrieving ip_whitelist: " .. err);
else
ip_whitelist:flush_all();
for index, banned_ip in ipairs(new_ip_whitelist) do
ngx.log(ngx.ERR,"redis read ip_whitelist:",banned_ip);
ip_whitelist:set(banned_ip, true);
end
ip_whitelist:set("last_update_time", ngx.now());
end
end
end
--判断ip是否在白名单中,不在则直接拒绝处理
if not (ip_whitelist:get("\""..ip.."\"")) then
ngx.log(ngx.ERR, "Banned IP detected and refused access: " .. ip);
ngx.status = 403;
ngx.header.content_type = "text/plain";
return ngx.print("access denied");
--return ngx.exit(ngx.HTTP_FORBIDDEN)
end
在location模块引用文件即可:
location /hello {
default_type 'text/plain';
access_by_lua_file /usr/local/openresty/nginx/conf/lua/ip_limitlist.lua;
content_by_lua 'ngx.say("hello,lua")';
}
需要lua支持geoIp2调用需要安装桥接依赖:
/usr/local/openresty/bin/opm get anjia0532/lua-resty-maxminddb
执行安装命令报错,缺少Digest/MD5依赖,安装即可:
yum -y install perl-Digest-MD5
安装成功后,继续执行前边的安装桥接命令:
GeoIP2 lua库依赖动态库安装,lua库依赖libmaxminddb实现对mmdb的高效访问。需要编译该库并添加到openresty访问环境。可以从https://github.com/maxmind/libmaxminddb/releases下载相应源码包到本地编译部署:
cd /opt/tools/
wget https://github.com/maxmind/libmaxminddb/releases/download/1.7.1/libmaxminddb-1.7.1.tar.gz
cd /libmaxminddb-1.7.1
./configure
make
make check
make install
ldconfig
默认情况下上述操作会将libmaxminddb.so部署到/usr/local/lib目录下,为了让openresty访问,可以拷贝到openresty目录下,或通过如下步骤更新ldconfig:
sh -c "echo /usr/local/lib >> /etc/ld.so.conf.d/local.conf"
ldconfig
把geoIp上传到服务器/opt/tools/geoIp2目录:
scp -r ./GeoLite2-Country_20230203.tar.gz root@xxx:/opt/tools/geoIp2
解压得到目录/opt/tools/geoIp2/GeoLite2-Country_20230203/GeoLite2-Country.mmdb
编写操作geoIp的lua脚本:
local cjson=require 'cjson'
local geo=require 'resty.maxminddb'
local ip = ngx.var.remote_addr
local isPh = false
if not geo.initted() then
geo.init("/opt/tools/geoIp2/GeoLite2-Country_20230203/GeoLite2-Country.mmdb")
end
local res,err=geo.lookup(ip)
if not res then
ngx.say("获取客户端ip失败,或当前请求的ip不是公网ip")
ngx.log(ngx.ERR,' failed to lookup by ip , reason :',err)
else
for k,v in pairs(res) do
--只获取国家
if(k == "country") then
--获取国家编码
for key,item in pairs(v) do
if (key=="iso_code" and "PH"== item) then
isPh=true
print("isPh=",isPh)
-- ngx.say("ip是菲律宾")
end
end
end
end
end
ngx.log(ngx.ERR,"ip:" .. ip);
--ngx.say("ip is ph:".. isPh);
ngx.exit(200)
通过redis白名单和geoIp检查用户ip归属地址,分别验证了请求访问ip限制,那么我们要做的是,先检查ip白名单,如果加了白直接放过,如果没加白则利用lua操作geoIp检查ip是否是菲律宾,如果是则放过,否则禁止访问:
废话不多说,直接上菜,编写操作redis白名单和geoIp的合并脚本:
local cjson=require 'cjson'
local geo=require 'resty.maxminddb'
local redis_host = "host"
local redis_port = 6379
local redis_connection_timeout = 100
local redis_key = "ip_whitelist_key"
local cache_ttl = 1
-- local headers = ngx.req.get_headers();
local ip = ngx.var.remote_addr
local ip_whitelist = ngx.shared.ip_whitelist
local last_update_time = ip_whitelist:get("last_update_time");
local inWhiteList = false
if last_update_time == nil or last_update_time < ( ngx.now() - cache_ttl ) then
local redis = require "resty.redis";
local red = redis:new();
red:set_timeout(redis_connect_timeout);
local ok, err = red:connect(redis_host, redis_port);
if not ok then
ngx.log(ngx.ERR, "Redis connection error while retrieving ip_whitelist: " .. err);
else
local new_ip_whitelist, err = red:smembers(redis_key);
if err then
ngx.log(ngx.ERR, "Redis read error while retrieving ip_whitelist: " .. err);
else
ip_whitelist:flush_all();
for index, banned_ip in ipairs(new_ip_whitelist) do
--ngx.log(ngx.ERR,"redis read ip_whitelist:",banned_ip);
ip_whitelist:set(banned_ip, true);
end
ip_whitelist:set("last_update_time", ngx.now());
end
end
end
--判断ip是否在白名单中,不在则直接拒绝处理
if not (ip_whitelist:get("\""..ip.."\"")) then
ngx.log(ngx.ERR, "Banned IP detected and refused access: " .. ip);
-- ngx.status = 403;
-- ngx.header.content_type = "text/plain";
-- return ngx.print("access denied");
local isPh = false
if not geo.initted() then
geo.init("/opt/tools/geoIp2/GeoLite2-Country_20230203/GeoLite2-Country.mmdb")
end
local res,err=geo.lookup(ip)
if not res then
-- ngx.say("获取客户端ip失败,或当前请求的ip不是公网ip")
ngx.log(ngx.ERR,' failed to lookup by ip , reason :',err)
else
for k,v in pairs(res) do
--只获取国家
if(k == "country") then
--获取国家编码
for key,item in pairs(v) do
if (key=="iso_code" and "PH"== item) then
isPh=true
print("isPh=",isPh)
-- ngx.say("ip是菲律宾")
end
end
end
end
end
if isPh==false then
ngx.log(ngx.ERR,"ip not in whitelist and not ph:"..ip)
ngx.status = 403;
ngx.header.content_type = "text/plain";
return ngx.print("access denied");
end
end
在http模块添加依赖引用:
lua_shared_dict ip_whitelist 1m;
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
然后在请求访问的位置放置ip校验脚本:
access_by_lua_file /usr/local/openresty/nginx/conf/lua/ip_limitlist.lua;
access_by_lua_file指令也可以放到http、location中,实现整个http服务或者指定URL的屏蔽。
下载自动更新程序:
cd /opt/tools
wget https://github.com/maxmind/geoipupdate/releases/download/v4.10.0/geoipupdate_4.10.0_linux_386.tar.gz
tar -xzvf geoipupdate_4.10.0_linux_386.tar.gz
将其安装到/usr/local/bin目录下:
cp /opt/tools/geoipupdate_4.10.0_linux_386/geoipupdate /usr/local/bin/
执行geoipupdate -v,报错,找不到/usr/local/etc/GeoIp.conf,那么创建该配置文件,参考:
https://dev.maxmind.com/geoip/updating-databases?lang=en#2-obtain-geoipconf-with-account-information
vim /usr/local/etc/GeoIp.conf
AccountID xxx
LicenseKey xxx
EditionIDs GeoLite2-Country
报错执行geoipupdate -v,报错找不到/usr/local/share/GeoIp目录,用来存储更新下载的ip库文件,创建即可:
cd /usr/local/share
mkdir GeoIP
继续执行geoipupdate -v,可以看到/usr/local/share/GeoIP目录下载了ip库文件:
基于linux自带crontab调度每个月更新geoIp库:
crontab -e
//在最后添加新任务
0 1 1 * * /usr/local/bin/geoipupdate
每个月1号凌晨1点执行执行(geoIp库官方免费版,每个月更新一次)。
保存并重启调度使其生效:
service crond restart
geoip自动更新程序会把ip库文件下载到/usl/local/share/GeoIP目录,在使用的地方比如lua脚本按需修改目录:
这样就可以使用到最新的ip库了。
还有一种方式是让自动更新程序下载到自定义目录:
go build -ldflags "-X main.defaultConfigFile=/etc/GeoIP.conf \
-X main.defaultDatabaseDirectory=/opt/tools/geoIp2/GeoLite2-Country_20230203/"
建议使用第一种,修改默认更新位置未经验证。
https://blog.csdn.net/weixin_41092791/article/details/107945032
https://blog.bruce121.com/install-openresty-and-replace-old-nginx-that-installing-by-yum.html
https://learnku.com/articles/44736
https://www.shuzhiduo.com/A/lk5axX1PJ1/
https://cloud.tencent.com/developer/article/1922048
https://cloud.tencent.com/developer/article/1050222
https://blog.csdn.net/chiken6443/article/details/100644083
https://pdf.us/2020/04/18/3969.html
https://dev.maxmind.com/geoip/updating-databases?lang=en#2-obtain-geoipconf-with-account-information
https://blog.51cto.com/u_3664660/3215015
https://blog.51cto.com/mshxuyi/5858605?articleABtest=1
https://github.com/openresty/lua-nginx-module/issues/1984
https://www.24kplus.com/linux/608.html
https://codeantenna.com/a/Wyx8cFv1Se
https://www.maoyingdong.com/nginx-ip-blacklist-based-on-redis/
https://www.osyunwei.com/archives/12543.html
https://blog.csdn.net/zhannk/article/details/84871043
https://blog.csdn.net/yinjinshui/article/details/118702545
https://blog.csdn.net/jack85986370/article/details/54292977
本文分享自 PersistentCoder 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!