zl程序教程

您现在的位置是:首页 >  数据库

当前栏目

openresty基于lua/geoIp/redis实现ip限制

RedisIP 实现 基于 限制 lua OpenResty GeoIP
2023-06-13 09:17:21 时间

内容目录

一、问题背景二、聊一嘴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过滤。

实现步骤

  1. 网关安装和编译lua+redis模块
  2. lua+redis连接线上redis
  3. 网关机器安装geoIp库
  4. 网关机器编写定时任务crontab,每个月更新geoIp库
  5. nginx基于lua redis模块编写白名单过滤规则
  6. nginx基于geoIp模块编写ip过滤规则
  7. 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