nginx+lua(OpenResty),实现访问限制
因发现部署在云平台上的业务日志中,不定时会有同一IP发送大量的正常请求的情况,导致服务器资源占用突然增高,由于程序端没做请求频率的限制,云服务上没有waf,故使用nginx+lua(OpenResty)+redis来做访问控制,发现请求频率高的IP,直接封掉,禁止访问。
一、部署OpenResty
1、安装工具和依赖
yum -y install wget vim gcc pcre-devel openssl-devel
2、从 https://github.com/openresty/openresty,下载OpenResty,这里使用openresty-1.25.3.1版本。
wget https://github.com/openresty/openresty/releases/download/v1.25.3.1/openresty-1.25.3.1.tar.gz
3、解压、配置、编译、安装
tar zxf openresty-1.25.3.1.tar.gz cd openresty-1.25.3.1 ./configure --prefix=/data/openresty gmake && gmake install
二、部署redis
用redis来存储被封的IP,当然也可以考虑使用其他的方式,这里使用redis,假设redis密码为123456,传送阵:部署redis
三、在nginx配置文件中添加lua脚本,启动nginx
1、在nginx配置文件中location模块里加入lua代码
vim /data/openresty/nginx/conf/nginx.conf
http { .... #创建一个共享内存区域来存储IP访问频率 lua_shared_dict limit_req_store 10m; server { listen 80; .... location / { root html; index index.html index.htm; #添加lua脚本,access_by_lua_block指令 access_by_lua_block { ngx.header.content_type = "text/plain; charset=utf-8" local request_limit = 50 -- 定义每个IP的请求频率限制变量 local ttl = 1 -- 定义缓存过期时间变量,单位秒 local redis_passwd = "123456" -- 定义redis密码 local redis_host = "192.168.1.11" -- 定义redis服务器地址 local redis_port = 6379 -- 定义redis端口信息 local redis_set_key = "limit_ip" -- 定义redis集合的key信息 local redis = require "resty.redis" local red = redis:new() red:set_timeout(1000) local ok, err = red:connect(redis_host, redis_port) if not ok then ngx.log(ngx.ERR,"连接redis出错: ", err) return end ok , err = red:auth(redis_passwd) if not ok then ngx.log(ngx.ERR,"redis授权失败:", err) return end local cardinality ,err = red:scard(redis_set_key) if cardinality == 0 then local my_limit = ngx.shared.my_limit_req_store local client_ip = ngx.var.remote_addr local request_count, _ = my_limit:get(client_ip) if not request_count then request_count = 1 else request_count = request_count + 1 end my_limit:set(client_ip,request_count,ttl) if request_count > request_limit then red:sadd(redis_set_key,client_ip) ngx.status = ngx.HTTP_TOO_MANY_REQUESTS ngx.log(ngx.ERR,"该IP请求频率过高,已被禁止访问!",client_ip) ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS) end else local client_ip = ngx.var.remote_addr local is_member, err = red:sismember(redis_set_key, client_ip) if err then ngx.log(ngx.ERR,"检查IP是否存在于redis中出错: ", err) return end if is_member == 0 then local my_limit = ngx.shared.my_limit_req_store local client_ip = ngx.var.remote_addr local request_count, _ = my_limit:get(client_ip) if not request_count then request_count = 1 else request_count = request_count + 1 end my_limit:set(client_ip,request_count,ttl) if request_count > request_limit then red:sadd(redis_set_key,client_ip) ngx.status = ngx.HTTP_TOO_MANY_REQUESTS ngx.log(ngx.ERR,"该IP请求频率过高,已被禁止访问!",client_ip) ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS) end else ngx.status = ngx.HTTP_FORBIDDEN ngx.log(ngx.ERR,"该IP已被禁止访问!",client_ip) ngx.exit(ngx.HTTP_FORBIDDEN) end end red:close() } } #配置content_type header_filter_by_lua_block { local content_type = ngx.header.content_type if ngx.var.uri:match("%.css$") then ngx.header.content_type = 'text/css; charset=utf-8' elseif ngx.var.uri:match("%.js$") then ngx.header.content_type = 'application/javascript; charset=utf-8' elseif ngx.var.uri:match("%.html$") or ngx.var.uri == "/" then ngx.header.content_type = 'text/html; charset=utf-8' else ngx.header.content_type = content_type end } } }
脚本说明:
a、使用了redis的集合来存储封禁IP的信息
b、access_by_lua_block 指令代码块可以放在http、server、location等模块下,来控制代码影响范围。
c、request_limit参数的值和ttl的值来控制访问频率限制,以上代码是限制每1秒钟50次请求,超过限制后,客户端IP将被禁止请求,直接报错403。
d、使用一个静态的html页面(没有css、js等代码)进行测试,请求1次,会执行2次access_by_lua_block指令,未找出具体原因,因此request_limit的值如果为20,ttl为1,那么该测试在1秒内限制请求10次。
e、该脚本对IP地址进行永久封禁(除非redis的数据丢失),如果想要实现限制时长功能,可以用其他方式存储,该方式的解封方式就只能操作redis来删除数据。
2、启动nginx
/data/openresty/nginx/sbin/nginx