openresty基于lua/geoIp/redis实现ip限制
内容目录
一、问题背景二、聊一嘴ip库三、解决方案四、下载安装openresty五、验证效果六、geoIp自动更新七、参考
一、问题背景
在一些中小型项目,会使用nginx作为流量和业务网关,实现流量分发、代理穿透以及负载等能力,当然也可以做一些流量管控和ip过滤限制等能力。
有些出海业务,其相关产品能力和业务接口只对某些国家ip开放,那么我们本着在离用户最近的位置过滤和防控原则,考虑在nginx做一些事情来实现ip识别和限制。
二、聊一嘴ip库
全球范围内,每天ip都在增长,但是不同的区域、不同的国家的ip增长,理论上要满足一定的规则,但是实际增长规则和期望通常会发生违背,所有目前来说暂时没有找到一统江湖并且和实际情况相匹配的规则。
退而言之,现在有很多组织都在维护ip库来做ip定位和过滤的事情,当然也有很多开源组织在做,目前来看市面上主流的如下:
- ipip.net [收费]
官网:http://www.ipip.net/
优势:支持离线下载,数据更新频繁,数据精确
劣势:收费
- geoIp [免费和收费]
官网:http://dev.maxmind.com/zh-hans/geoip/legacy/geolite/
优势:支持离线下载,数据如果是收费版的更新频繁,数据精确,有大致经纬度
劣势:免费版更新不频繁,大概每个月更新一次
- QQWry[免费]
优势:免费,支持离线下载,数据显示到省市
劣势:数据更新不频繁(有些地址查不到)
- qqzeng [收费]
官网:http://qqzeng.com/
优势:支持离线下载,数据更新频繁,数据精确
劣势: 收费
- ip2region[免费]
优势:免费,性能好
劣势: 数据更新不频繁,频率为止
geoIp免费版基本够用。
三、解决方案
综合考虑技术现状和业务诉求,解决方案理应遵守以下原则:
- 免费开源
- 要支持更新,优先考虑有定期更新和频繁更新
- 在距离用户最近的位置做过滤和ip甄别
- 整个调用链路都要悲观的认为前一个节点存在漏网之鱼,每一个节点都要考虑做过滤
所以我们一期初步选定基于geoIp免费版来实现:
名称 | 每秒ip查询速度 | 命中率 | 更新频率 |
---|---|---|---|
geoIp(免费版) | 1.8039w/s | 99.65% | 每月更新 |
geoIp免费版支持的能力和特性,基本能够满足我们的业务诉求。
目前我们每个app都有一个nginx网关,那么基于 在离用户最近的位置做过滤 原则,我们可以考虑使用nginx + geoIp来做ip过滤。
实现步骤
- 网关安装和编译lua+redis模块
- lua+redis连接线上redis
- 网关机器安装geoIp库
- 网关机器编写定时任务crontab,每个月更新geoIp库
- nginx基于lua redis模块编写白名单过滤规则
- nginx基于geoIp模块编写ip过滤规则
- nginx开启定期检查和加载geoIp库
注意:5和6属于核心规则配置,并且有执行优先级,如果ip在白名单,直接放过,如果不在白名单则执行geoIp过滤规则。
四、下载安装openresty
1.下载openresty
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安装并启动成功了。
五、验证效果
1.验证lua模块
在配置文件server模块添加:
location ^~ /lua/hello{
default_type 'text/plain';
content_by_lua 'ngx.say("Hello, Lua!")';
}
发送请求:
curl http://localhost:80/lua/hello
2.验证lua redis
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")';
}
3.验证lua geoIp2
需要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)
4.验证lua+redis+geoIp2
通过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的屏蔽。
六、geoIp自动更新
1.安装geoIp自动更新包
下载自动更新程序:
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库文件:
2.配置自动下载任务
基于linux自带crontab调度每个月更新geoIp库:
crontab -e
//在最后添加新任务
0 1 1 * * /usr/local/bin/geoipupdate
每个月1号凌晨1点执行执行(geoIp库官方免费版,每个月更新一次)。
保存并重启调度使其生效:
service crond restart
3.补充
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
相关文章
- Redis实战:中文PDF版(redis实战中文pdf)
- 捍卫win 32,Redis激情活动(win 32 redis)
- 支撑让Redis起到最大支撑力的秘诀(r如何起redis)
- 简单精准快速部署Redis 内存服务器配置实践(内存服务器redis配置)
- Redis常识一知半解不妨参考一下(关于redis的常识)
- 如何洞察Redis事务的奥秘(如何理解redis事务)
- 会话管理系统搭建用Redis实现会话共享(会话共享 redis)
- 基于Redis瞬间抢购,实现快速分布式秒杀(基于redis处理秒杀)
- Redis实现高效率数据存储(redis 高效率存储)
- 应用Redis实现极致高可用(redis高可用锁)
- 实现Redis队列自动删除功能(redis队列自动删除)
- 使用Redis实现消息队列(redis 队列实例)
- 利用Redis键值解锁技巧之旅(redis键值技巧)
- 细节决定成败深入浅出掌握Redis锁机制(redis锁机制6)
- 基于SSM架构集成Redis实现高效缓存管理(ssm集成redis实战)
- 模式Redis 实现快速的批量退出订阅模式(redis 退出所有订阅)
- 利用Redis优化系统缓存性能(redis进行缓存)
- Redis实现MySQL数据读取加速(redis读取mysql)
- Redis实现轻松的持久层(redis落持久层)
- 深入了解Redis缓存命中与失败(redis缓存命中失败)