如何通过Lua+Redis 实现动态封禁IP?

发布时间:2021-09-21

  openresty开发系列38--通过Lua+Redis 实现动态封禁IP一)需求背景为了封禁某些爬虫或者恶意用户对服务器的请求,我们需要建立一个动态的 IP 黑名单。对于黑名单之内的 IP ,拒绝提供服务。二)设计方案实现 IP 黑名单的功能有很多途径:1、在操作系统层面,配置 iptables,拒绝指定 IP 的网络请求;2、在 Web Server 层面,通过 Nginx 自身的 deny 选项 或者 lua 插件 配置 IP 黑名单;3、在应用层面,在请求服务之前检查一遍客户端 IP 是否在黑名单。为了方便管理和共享,我们通过 Nginx+Lua+Redis 的架构实现 IP 黑名单的功能如图

  

  

  

  配置f在http部分,配置本地缓存,来缓存redis中的数据,避免每次都请求redislua_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设置了密码时代码如下:

  []# 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代码:

  []# 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 --如果超过单位时间限制的访问次数,则添加限制访问标识,www.lu0.com限制时间为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 中的黑名单。

注册即送1000元现金券