孙宇的技术专栏 大数据/机器学习

nginx+lua防爬虫

2015-12-17

阅读:


通过 lua ,在 nginx 里结合 redis 进行请求频率等判断。防止请求进入服务层并压垮服务。

这里的判断逻辑很简单,判定是非法请求的依据有两点:

  1. 60 秒内请求达到 40 个
  2. 3600 秒内请求达到 500 个

相关IP及请求数都记在 redis 里。

nginx 配置

server {
    listen       80;
    server_name  lua.test.cn;
    root    /home/httpd/sites/web;

    location /lua {
			default_type 'text/html';
			lua_code_cache off;
			access_by_lua_file /usr/local/openresty/nginx/conf/limits/access_block.lua;
    }
}

防爬虫代码

access_block.lua

local config = require("common_config")
local redis = require("ngx_redis")

--封IP规则:一分钟访问超过 40次; 一小时访问超过 500 次
ip_block_time = 600  --封禁IP时间

ip_time_out = 60    --指定ip访问频率时间段
connect_count = 40 --指定ip访问频率计数最大值

ip_time_out_h = 3600
connect_count_h = 500

--连接redis
local cache = redis:create_connection()
if cache == 0 then
  goto A
end

--白名单处理.如果IP在白名单内,直接放过
white_ip = {"100.97.", "123.125.71.", "42.236."}

is_white = "0"
for k, val in pairs(white_ip) do
  local r = ngx.re.match(ngx.var.remote_addr, "^"..val..".*?$")
  if r then
    is_white = "1"
    local ok, err = cache:close()
    break;
  end
end

if is_white == "0" then
  --查询ip是否在封禁段内,若在则返回403错误代码
  --因封禁时间会大于ip记录时间,故此处不对ip时间key和计数key做处理
  is_block , err = cache:get("block_"..ngx.var.remote_addr)
  if is_block == "1" then
    ngx.exit(403)
    goto A
  end
  start_time , err = cache:get("time_"..ngx.var.remote_addr)
  ip_count , err = cache:get("count_"..ngx.var.remote_addr)
  --如果ip记录时间大于指定时间间隔或者记录时间或者不存在ip时间key则重置时间key和计数key
  --如果ip时间key小于时间间隔,则ip计数+1,且如果ip计数大于ip频率计数,则设置ip的封禁key为1
  --同时设置封禁key的过期时间为封禁ip的时间
  if start_time == ngx.null or os.time() - start_time > ip_time_out then
    res , err = cache:set("time_"..ngx.var.remote_addr , os.time())
    res , err = cache:set("count_"..ngx.var.remote_addr , 1)
  else
    ip_count = ip_count + 1
    res , err = cache:incr("count_"..ngx.var.remote_addr)
    if ip_count >= connect_count then
      res , err = cache:set("block_"..ngx.var.remote_addr,1)
      res , err = cache:expire("block_"..ngx.var.remote_addr,ip_block_time)
    end
  end


  --查询ip是否在封禁段内,若在则返回403错误代码
  --因封禁时间会大于ip记录时间,故此处不对ip时间key和计数key做处理
  is_blockh_h , err_h = cache:get("block_h_"..ngx.var.remote_addr)
  if is_block_h == "1" then
    ngx.exit(403)
    goto A
  end
  start_time_h , err_h = cache:get("time_h_"..ngx.var.remote_addr)
  ip_count_h , err_h = cache:get("count_h_"..ngx.var.remote_addr)
  --如果ip记录时间大于指定时间间隔或者记录时间或者不存在ip时间key则重置时间key和计数key
  --如果ip时间key小于时间间隔,则ip计数+1,且如果ip计数大于ip频率计数,则设置ip的封禁key为1
  --同时设置封禁key的过期时间为封禁ip的时间
  if start_time_h == ngx.null or os.time() - start_time_h > ip_time_out_h then
    res_h , err_h = cache:set("time_h_"..ngx.var.remote_addr , os.time())
    res_h , err_h = cache:set("count_h_"..ngx.var.remote_addr , 1)
  else
    ip_count_h = ip_count_h + 1
    res_h , err_h = cache:incr("count_h_"..ngx.var.remote_addr)
    if ip_count_h >= connect_count_h then
      res_h , err_h = cache:set("block_h_"..ngx.var.remote_addr,1)
      res_h , err_h = cache:expire("block_h_"..ngx.var.remote_addr,ip_block_time)
    end
  end
end

--结尾标记
::A::
local ok, err = cache:close()

可以看出,代码里引用了两个其它的模块,common_config 和 ngx_redis。它们代码分别是:

全局配置代码

common_config.lua:

--全局配置
--局部变量.模块名称
local _M = {}

function _M.getRedisHost(self)
  --return "100.92.113.11"
  return "f2f163efe1ic11g4.m.cnhza.kvstore.aliyuncs.com"
end

function _M.getRedisPort(self)
  return "6379"
end

function _M.getRedisPwd(self)
  return "f2f163efe1ic11g4:yourpwd"
end

return _M

redis 连接池

ngx_redis.lua:

--Nginx-redis 封装组件.提供连接池之类的功能
local config = require("common_config")
local redis = require("resty.redis")

--局部变量.模块名称
local _M = {}

function _M.create_connection(self)
	local host = config:getRedisHost()
	local port = config:getRedisPort()
	local pwd = config:getRedisPwd()
--	ngx.say(host, " : ", port)
	--创建实例
	local red = redis:new()
	--设置超时(毫秒)
	red:set_timeout(1000)
	--建立连接
	local ok, err = red:connect(host, port)
	red:auth(pwd)

	if not ok then
	    ngx.say("connect to redis error : ", err)
	    return 0
	    --return close_connection(red)
	end
--	ngx.say("connect success!", host, port)
	return red
end

function _M.close_connection(self, conn)
	if not conn then
        return
    end
    local ok, err = conn:close()
    if not ok then
		return 0
    end
    return 1
    --释放连接(连接池实现)
    --local pool_max_idle_time = 100000 --毫秒
    --local pool_size = 100 --连接池大小
    --local ok, err = conn:set_keepalive(pool_max_idle_time, pool_size)
    --if not ok then
    --    return 0
    --end
	--return 1
end

return _M

可以看到,这里自己做了连接池管理。它是以 resty.redis 为基础的。所以要先安装这个模块。


上一篇 nginx+lua

下一篇 秒杀系统构建

评论

文章