OpenResty+ Nginx + Lua + Redis 动态封禁 IP

之前有试过实现如何封禁某个国家的IP访问网站。最近在看如何封禁某些特定IP对资源的访问,于是整理了一份通过Nginx + Lua + Redis 动态封禁 IP的资料。

需求背景

为了封禁某些爬虫或者恶意用户对服务器的请求,需要建立一个动态的 IP 黑名单。对于黑名单之内的 IP ,拒绝提供服务。

实现方式

实现 IP 黑名单的功能有很多方法:

  • 1.在操作系统层面,配置 iptables,拒绝指定 IP 的网络请求;
  • 2.在 Web Server 层面,通过 Nginx 自身的 deny 选项 或者 lua 插件 配置 IP 黑名单;
  • 3.在应用层面,在请求服务之前检查一遍客户端 IP 是否在黑名单。
  • 4.接入层,通过OpenResty的Nginx + Lua 模块实现动态封禁 IP

实现:通过OpenResty的Nginx+ Lua模块

OpenResty,是一个集成了各种 Lua 模块的 Nginx 服务器。也可以通过,安装 Nginx+Lua模块。

lua_shared_dict 指令介绍

声明一个共享内存区域 name,以充当基于 Lua 字典 ngx.shared. 的共享存储。
共享内存总是被当前 Nginx 服务器实例中所有的 Nginx worker 进程所共享。
size 参数接受大小单位,如 k,m:

lua_shared_dict 官方使用示例:

 http {
     lua_shared_dict dogs 10m;
     server {
         location /set {
             content_by_lua_block {
                 local dogs = ngx.shared.dogs
                 dogs:set("Jim", 8)
                 ngx.say("STORED")
             }
         }
         location /get {
             content_by_lua_block {
                 local dogs = ngx.shared.dogs
                 ngx.say(dogs:get("Jim"))
             }
         }
     }
 }

配置nginx.conf

在http部分,配置本地缓存,来缓存redis中的数据,避免每次都请求redis

lua_shared_dict shared_ip_blacklist 8m; #定义ip_blacklist 本地缓存变量

location /ipblacklist {
access_by_lua_file /usr/local/lua/access_by_limit_ip.lua;
echo “ipblacklist”;
}

lua脚本:

# 编辑 /usr/local/lua/access_by_limit_ip.lua

local function close_redis(red)
    if not red then  
        return
    end  
    --释放连接(连接池实现)  
    local pool_max_idle_time = 10000 --毫秒  
    local pool_size = 100 --连接池大小  
    local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)  
    if not ok then  
        ngx.say("set keepalive error : ", err)  
    end  
end

local function errlog(...)
    ngx.log(ngx.ERR, "redis: ", ...)
end

local function duglog(...)
    ngx.log(ngx.DEBUG, "redis: ", ...)
end

local function getIp()
    local myIP = ngx.req.get_headers()["X-Real-IP"]
    if myIP == nil then
        myIP = ngx.req.get_headers()["x_forwarded_for"]
    end
    if myIP == nil then
        myIP = ngx.var.remote_addr
    end
    return myIP;
end

local key = "limit:ip:blacklist"
local ip = getIp();
local shared_ip_blacklist = ngx.shared.shared_ip_blacklist

--获得本地缓存的最新刷新时间
local last_update_time = shared_ip_blacklist:get("last_update_time");

if last_update_time ~= nil then 
    local dif_time = ngx.now() - last_update_time 
    if dif_time < 60 then --缓存1分钟,没有过期
        if shared_ip_blacklist:get(ip) then
            return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403
        end
        return
    end
end

local redis = require "resty.redis"  --引入redis模块
local red = redis:new()  --创建一个对象,注意是用冒号调用的

--设置超时(毫秒)  
red:set_timeout(1000) 
--建立连接  
local ip = "10.11.0.215"  
local port = 6379
local ok, err = red:connect(ip, port)
if not ok then  
    close_redis(red)
    errlog("limit ip cannot connect redis");
else
    local ip_blacklist, err = red:smembers(key);

    if err then
        errlog("limit ip smembers");
    else
        --刷新本地缓存,重新设置
        shared_ip_blacklist:flush_all();

        --同步redis黑名单 到 本地缓存
        for i,bip in ipairs(ip_blacklist) do
            --本地缓存redis中的黑名单
            shared_ip_blacklist:set(bip,true);
        end
        --设置本地缓存的最新更新时间
        shared_ip_blacklist:set("last_update_time",ngx.now());
    end
end  

if shared_ip_blacklist:get(ip) then
    return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403
end

这个脚本可以实现手动地动态地封禁IP。

自动封禁访问频次过高的IP地址

改进后的代码,可以实现自动将访问频次过高的IP地址加入黑名单封禁一段时间

local function close_redis(red)
    if not red then
        return
    end

    local pool_max_idle_time = 10000
    local pool_size = 100
    local ok, err = red:set_keepalive(pool_max_idle_tme, pool_size)
    if not ok then
        ngx.say("set keepalive err : ", err)
    end
end


local ip_block_time=300 --封禁IP时间(秒)
local ip_time_out=30    --指定ip访问频率时间段(秒)
local ip_max_count=20 --指定ip访问频率计数最大值(秒)
local BUSINESS = ngx.var.business --nginx的location中定义的业务标识符

--连接redis
local redis = require "resty.redis"  
local conn = redis:new()  
ok, err = conn:connect("10.11.0.215", 6379)  
conn:set_timeout(2000) --超时时间2秒

--如果连接失败,跳转到脚本结尾
if not ok then
    --goto FLAG
   close_redis(conn)
end

local count, err = conn:get_reused_times()
if 0 == count then ----新建连接,需要认证密码
    ok, err = conn:auth("redis123")
    if not ok then
        ngx.say("failed to auth: ", err)
        return
    end
elseif err then  ----从连接池中获取连接,无需再次认证密码
    ngx.say("failed to get reused times: ", err)
    return
end

--查询ip是否被禁止访问,如果存在则返回403错误代码
is_block, err = conn:get(BUSINESS.."-BLOCK-"..ngx.var.remote_addr)  
if is_block == '1' then
    ngx.exit(403)
    close_redis(conn)
end

--查询redis中保存的ip的计数器
ip_count, err = conn:get(BUSINESS.."-COUNT-"..ngx.var.remote_addr)

if ip_count == ngx.null then --如果不存在,则将该IP存入redis,并将计数器设置为1、该KEY的超时时间为ip_time_out
    res, err = conn:set(BUSINESS.."-COUNT-"..ngx.var.remote_addr, 1)
    res, err = conn:expire(BUSINESS.."-COUNT-"..ngx.var.remote_addr, ip_time_out)
else
    ip_count = ip_count + 1 --存在则将单位时间内的访问次数加1

    if ip_count >= ip_max_count then --如果超过单位时间限制的访问次数,则添加限制访问标识,限制时间为ip_block_time
        res, err = conn:set(BUSINESS.."-BLOCK-"..ngx.var.remote_addr, 1)
        res, err = conn:expire(BUSINESS.."-BLOCK-"..ngx.var.remote_addr, ip_block_time)
    else
        res, err = conn:set(BUSINESS.."-COUNT-"..ngx.var.remote_addr,ip_count)
        res, err = conn:expire(BUSINESS.."-COUNT-"..ngx.var.remote_addr, ip_time_out)
    end
end

-- 结束标记
local ok, err = conn:close()

这个脚本的逻辑:
第一步:检查该IP是否在封禁列表当中,如在,而返回403
第二部:检查该IP是否频率过高,(通过查询访问计数器:redis counter)

总结

通过 Nginx+Lua+Redis 实现的 IP 黑名单功能,具有如下优点:

  • 1.配置简单、轻量,几乎对服务器性能不产生影响;
  • 2.多台服务器可以通过Redis实例共享黑名单;
  • 3.动态配置,可以手工或者通过某种自动化的方式设置 Redis 中的黑名单。

参考:
1. OpenResty lua_shared_dict官方介绍 https://github.com/openresty/lua-nginx-module#ngxshareddict
2. Lua脚本 :https://www.cnblogs.com/reblue520/p/11419918.html
(完)

发表评论

邮箱地址不会被公开。 必填项已用*标注