• openresty开发系列38--通过Lua+Redis 实现动态封禁IP


    openresty开发系列38--通过Lua+Redis 实现动态封禁IP

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

    二)设计方案
    实现 IP 黑名单的功能有很多途径:
    1、在操作系统层面,配置 iptables,拒绝指定 IP 的网络请求;
    2、在 Web Server 层面,通过 Nginx 自身的 deny 选项 或者 lua 插件 配置 IP 黑名单;
    3、在应用层面,在请求服务之前检查一遍客户端 IP 是否在黑名单。

    为了方便管理和共享,我们通过 Nginx+Lua+Redis 的架构实现 IP 黑名单的功能

    如图

    配置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";
    }


    # 编辑 /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
    
    

    当redis设置了密码时代码如下:

    [root@node5 lua]# cat /usr/local/lua/access_by_limit_ip.lua

    local function close_reis(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
            if shared_ip_blacklist:get(ip) then
                return ngx.exit(ngx.HTTP_FORBIDDEN)
            end
            return
        end
    end
    
    local redis = require "resty.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)
    
    local count, err = red:get_reused_times()
    if 0 == count then ----新建连接,需要认证密码
        ok, err = red: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
    
    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();
    
            for i,bip in ipairs(ip_blacklist) do
                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)
    end

    用户redis客户端设置:
    添加黑名单IP:
    sadd limit:ip:blacklist 10.11.0.148

    获取黑名单IP:
    smembers limit:ip:blacklist


    10.11.0.215:6379> sadd limit:ip:blacklist 10.11.0.148
    10.11.0.215:6379> sadd limit:ip:blacklist 10.11.0.215

    10.11.0.215:6379> smembers limit:ip:blacklist
    1) "10.11.0.215"
    2) "10.11.0.148"
    10.11.0.215:6379> smembers limit:ip:blacklist
    1) "10.11.0.215"
    2) "10.11.0.148"


    此方法目前只能实现手动添加黑名单IP进行IP封禁,在某些场景如:半夜如果有人恶意爬取网站服务器可能导致服务器资源耗尽崩溃或者影响业务


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


    nginx.conf配置部分:
    location /goodslist {
            set $business "USER";
            access_by_lua_file /usr/local/lua/access_count_limit.lua;
            echo "get goods list success";
        }


    lua代码:

    [root@node5 lua]# cat /usr/local/luaaccess_count_limit.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_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()


    # redis的数据
    10.11.0.215:6379> get USER-COUNT-10.11.0.148
    "16"
    10.11.0.215:6379> get USER-BLOCK-10.11.0.148
    (nil)


    四、总结

    以上,便是 Nginx+Lua+Redis 实现的 IP 黑名单功能,具有如下优点:

    1、配置简单、轻量,几乎对服务器性能不产生影响;

    2、多台服务器可以通过Redis实例共享黑名单;

    3、动态配置,可以手工或者通过某种自动化的方式设置 Redis 中的黑名单。
  • 相关阅读:
    2014年7顶级编程语言一个月
    Swift学习 --- 2.3和字符串
    自己写CPU第五级(4)——逻辑、实现移动和空指令
    读书笔记-互联网思维阅读10其中一本书《自由》
    Linux学习笔记——如何使用共享库交叉编译
    Vim识别编码
    linux RWT
    GConf 错误:联系配置服务器失败;某些可能原因是需要为 ORBit 启用 TCP/IP 联网
    Mysql 表忽略大小写~~
    andorid studio
  • 原文地址:https://www.cnblogs.com/reblue520/p/11419918.html
Copyright © 2020-2023  润新知