欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Openresty通过Lua+Redis 实现动态封禁IP

程序员文章站 2022-05-03 19:17:43
...

需求背景

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

环境准备

linux version:centos7
redis version:5.0.5
Openresty:1.15.8.3
关于openresty安装可以参考我的另一个文章:OpenResty 1.15.8.3 安装使用

设计方案

实现 IP 黑名单的功能有很多途径:
1、在操作系统层面,配置 iptables,拒绝指定 IP 的网络请求;

  • 优点:简单暴力,直接在服务区物理层杜绝
  • 缺点:每次需要手动上服务器修改配置文件,麻烦,且不可控

2、在 Web Server 层面,通过 Nginx 自身的 deny 选项 或者 lua 插件 配置 IP 黑名单;

  • 优点:可动态实现封禁ip,通过设置封禁时间可以做到分布式封禁
  • 缺点:对Lua脚本、Nginx配置需要熟悉

3、在应用层面,在请求服务之前检查一遍客户端 IP 是否在黑名单。

  • 优点:直接写代码,简单可维护
  • 缺点:代码冗余且高并发会影响性能

最后采用方案

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

如图
Openresty通过Lua+Redis 实现动态封禁IP

配置nginx.conf

在需要限流配置的server下location配置

location /goodslist {
     # 如果该location 下存在静态资源文件可以做一个判断      
    #if ($request_uri ~ .*\.(html|htm|jpg|js|css)) {
    # access_by_lua_file /usr/local/lua/access_limit.lua;   
    #}
    
    access_by_lua_file /usr/local/lua/access_limit.lua;
    default_type  text/html;
    content_by_lua '
     ngx.say("<p>goodslist</p>")
    ';
}

编辑 /usr/local/lua/access_limit.lua

--
-- Created by IntelliJ IDEA.
-- User: libin
-- Date: 2020/6/9
-- Time: 16:07
-- To change this template use File | Settings | File Templates.
-- 可以实现自动将访问频次过高的IP地址加入黑名单封禁一段时间

--连接池超时回收毫秒
local pool_max_idle_time = 10000
--连接池大小
local pool_size = 100
--redis 连接超时时间
local redis_connection_timeout = 100
--redis host
local redis_host = "your redis host ip"
--redis port
local redis_port = "your redis port"
--redis auth
local redis_auth = "your redis authpassword";
--封禁IP时间(秒)
local ip_block_time= 120
--指定ip访问频率时间段(秒)
local ip_time_out = 1
--指定ip访问频率计数最大值(次)
local ip_max_count = 3


--  错误日志记录
local function errlog(msg, ex)
    ngx.log(ngx.ERR, msg, ex)
end

-- 释放连接池
local function close_redis(red)
    if not red then
        return
    end
    local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
    if not ok then
        ngx.say("redis connct err:",err)
        return red:close()
    end
end


--连接redis
local redis = require "resty.redis"
local client = redis:new()
local ok, err = client:connect(redis_host, redis_port)
-- 连接失败返回服务器错误
if not ok then
    close_redis(client)
    ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
--设置超时时间
client:set_timeout(redis_connection_timeout)

-- 优化验证密码操作 代表连接在连接池使用的次数,如果为0代表未使用,不为0代表复用 在只有为0时才进行密码校验
local connCount, err = client:get_reused_times()
-- 新建连接,需要认证密码
if  0 == connCount then
    local ok, err = client:auth(redis_auth)
    if not ok then
        errlog("failed to auth: ", err)
        return
    end
    --从连接池中获取连接,无需再次认证密码
elseif err then
    errlog("failed to get reused times: ", err)
    return
end

-- 获取请求ip
local function getIp()
    local clientIP = ngx.req.get_headers()["X-Real-IP"]
    if clientIP == nil then
        clientIP = ngx.req.get_headers()["x_forwarded_for"]
    end
    if clientIP == nil then
        clientIP = ngx.var.remote_addr
    end
    return clientIP
end

local cliendIp = getIp();

local incrKey = "limit:count:"..cliendIp
local blockKey = "limit:block:"..cliendIp

--查询ip是否被禁止访问,如果存在则返回403错误代码
local is_block,err = client:get(blockKey)
if tonumber(is_block) == 1 then
    ngx.exit(ngx.HTTP_FORBIDDEN)
    close_redis(client)
end

local ip_count, err = client:incr(incrKey)
if tonumber(ip_count) == 1 then
    client:expire(incrKey,ip_time_out)
end
--如果超过单位时间限制的访问次数,则添加限制访问标识,限制时间为ip_block_time
if tonumber(ip_count) > tonumber(ip_max_count) then
    client:set(blockKey,1)
    client:expire(blockKey,ip_block_time)
end

close_redis(client)

总结

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

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