自己搭建的网站网站刚上线 ,短信接口就被一直攻击,刚上攻击并且攻击者不停变换IP ,线被导致阿里云短信平台上的网站短信被恶意刷取了几千条 ,加上最近工作比较忙 ,刚上攻击就直接在OpenResty上对短信接口做了一些限制 ,线被采用OpenResty+Lua的网站方案成功动态封禁了频繁刷短信接口的IP。
由于事情比较紧急,线被所以 ,网站当发现这个问题时 ,刚上攻击就先采用快速的线被临时方案解决 。模板下载
(1)查看Nginx日志发现被攻击的网站IP 和接口
复制[root@binghe ~]# tail -f /var/log/nginx/access.log1.发现攻击者一直在用POST请求 /fhtowers/user/getVerificationCode这个接口
图片
(2)用awk和grep脚本过滤nginx日志,提取攻击短信接口的刚上攻击ip(一般这个接口是用来发注册验证码的,一分钟如果大于10次请求的线被话就不是正常的访问请求了,大家根据自己的实际情况更改脚本)并放到一个txt文件中去,然后重启nginx
复制[root@binghe ~]# cat denyip.sh #!/bin/bash nginx_home=/usr/local/openresty/nginx log_path=/var/log/nginx/access.log tail -n5000 $log_path | grep getVerification | awk { print $1} |sort | uniq -c | sort -nr -k1 | head -n 100 |awk { if($1>10)print ""$2""} >$nginx_home/denyip/blocksip.txt /usr/bin/nginx -s reload1.2.3.4.5.6.(3)设置Nginx去读取用脚本过滤出来的blocksip.txt(注意一下 ,我这里的Nginx是高防服务器用的openresty ,自带识别lua语法的 ,下面会有讲openresty的用法)
复制location = /fhtowers/user/getVerificationCode { #短信接口 access_by_lua local f = io.open("/usr/local/openresty/nginx/denyip/blocksip.txt") #黑名单列表 for line in f:lines() do if ngx.var.http_x_forwarded_for == line then #如果ip在黑名单列表里直接返回403 ngx.exit(ngx.HTTP_FORBIDDEN) end end ; proxy_pass http://appservers; #不在名单里就转发给后台的tomcat服务器 }1.2.3.4.5.6.7.8.9.10.11.(4)把过滤脚本放进crontab任务里,一分钟执行一次
复制[root@binghe ~]# crontab -e */1 * * * * sh /root/denyip.sh1.2.(5)查看一下效果,发现攻击者的请求都被返回403并拒绝了
图片
临时方案有效果后 ,再将其调整成使用OpenResty+Lua脚本的方案,来一张草图 。
图片
接下来,就是免费模板基于OpenResty和Redis实现自动封禁访问频率过高的IP。
安装使用 OpenResty,这是一个集成了各种 Lua 模块的 Nginx 服务器 ,是一个以Nginx为核心同时包含很多第三方模块的Web应用服务器 ,使用Nginx的同时又能使用lua等模块实现复杂的控制 。
(1)安装编译工具 、依赖库
复制[root@test1 ~]# yum -y install readline-devel pcre-devel openssl-devel gcc1.(2)下载openresty-1.13.6.1.tar.gz 源码包 ,并解压;下载ngx_cache_purge模块 ,该模块用于清理nginx缓存;下载nginx_upstream_check_module模块 ,建站模板该模块用于ustream健康检查。
复制[root@test1 ~]# cd /usr/local/ [root@test1 local]# wget https://openresty.org/download/openresty-1.13.6.1.tar.gz [root@test1 local]# tar -zxvf openresty-1.13.6.1.tar.gz [root@test1 local]# cd openresty-1.13.6.1/bundle [root@test1 local]# wget http://labs.frickle.com/files/ngx_cache_purge-2.3.tar.gz [root@test1 local]# tar -zxvf ngx_cache_purge-2.3.tar.gz [root@test1 local]# wget https://github.com/yaoweibin/nginx_upstream_check_module/archive/v0.3.0.tar.gz [root@test1 local]# tar -zxvf v0.3.0.tar.gz1.2.3.4.5.6.7.8.(3)配置需安装的模块
复制# ./configure --help可查询需要安装的模块并编译安装 [root@test1 openresty-1.13.6.1]# ./configure --prefix=/usr/local/openresty --with-luajit --with-http_ssl_module --user=root --group=root --with-http_realip_module --add-module=./bundle/ngx_cache_purge-2.3/ --add-module=./bundle/nginx_upstream_check_module-0.3.0/ --with-http_stub_status_module [root@test1 openresty-1.13.6.1]# make && make install1.2.3.(4)创建一个软链接方便启动停止
复制[root@test1 ~]# ln -s /usr/local/openresty/nginx/sbin/nginx /bin/nginx1.(5)启动nginx
复制[root@test1 ~]# nginx #启动 [root@test1 ~]# nginx -s reload #reload配置1.2.如果启动时候报错找不到PID的话就用以下命令解决(如果没有更改过目录的话 ,让它去读nginx的配置文件就好了)
复制[root@test1 ~]# /usr/local/openresty/nginx/sbin/nginx -c /usr/local/openresty/nginx/conf/nginx.conf1.
图片
随后,打开浏览器访问页面 。
图片
(6)在Nginx上测试一下能否使用Lua脚本
复制[root@test1 ~]# vim /usr/local/openresty/nginx/conf/nginx.conf1.在server里面加一个
复制location /lua { default_type text/plain; content_by_lua ‘ngx.say(“hello,lua!”)’; }1.2.3.4.
图片
加完后重新reload配置。
复制[root@test1 ~]# nginx -s reload1.在浏览器里输入 ip地址/lua ,出现下面的字就表示Nginx能够成功使用lua了
图片
(1)下载、解压、编译安装
复制[root@test1 ~]# cd /usr/local/ [root@test1 local]# wget http://download.redis.io/releases/redis-6.0.1.tar.gz [root@test1 local]# tar -zxvf redis-6.0.1.tar.gz [root@test1 local]# cd redis-6.0.1 [root@test1 redis-6.0.1]# make [root@test1 redis-6.0.1]# make install1.2.3.4.5.6.(2)查看是否安装成功
复制[root@test1 redis-6.0.1]# ls -lh /usr/local/bin/ [root@test1 redis-6.0.1]# redis-server -v Redis server v=3.2.5 sha=00000000:0 malloc=jemalloc-4.0.3 bits=64 build=dae2abf3793b309d1.2.3.(3)配置redis 创建dump file、源码下载进程pid、log目录
复制[root@test1 redis-6.0.1]# cd /etc/ [root@test1 etc]# mkdir redis [root@test1 etc]# cd /var/ [root@test1 var]# mkdir redis [root@test1 var]# cd redis/ [root@test1 redis]# mkdir data log run1.2.3.4.5.6.(4)修改配置文件
复制[root@test1 redis]# cd /usr/local/redis-6.0.1/ [root@test1 redis-6.0.1]# cp redis.conf /etc/redis/6379.conf [root@test1 redis-6.0.1]# vim /etc/redis/6379.conf #绑定的主机地址 bind 192.168.1.222 #端口 port 6379 #认证密码(方便测试不设密码 ,注释掉) #requirepass #pid目录 pidfile /var/redis/run/redis_6379.pid #log存储目录 logfile /var/redis/log/redis.log #dump目录 dir /var/redis/data #Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程 daemonize yes1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.(5)设置启动方式
复制[root@test1 redis-6.0.1]# cd /usr/local/redis-6.0.1/utils/ [root@test1 utils]# cp redis_init_script /etc/init.d/redis [root@test1 utils]# vim /etc/init.d/redis #根据自己实际情况修改1.2.3./etc/init.d/redis文件的内容如下。
复制#!/bin/sh # # Simple Redis init.d script conceived to work on Linux systems # as it does use of the /proc filesystem. REDISPORT=6379 EXEC=/usr/local/bin/redis-server CLIEXEC=/usr/local/bin/redis-cli PIDFILE=/var/run/redis_${ REDISPORT}.pid CONF="/etc/redis/${ REDISPORT}.conf" case "$1" in start) if [ -f $PIDFILE ] then echo "$PIDFILE exists, process is already running or crashed" else echo "Starting Redis server..." $EXEC $CONF fi ;; stop) if [ ! -f $PIDFILE ] then echo "$PIDFILE does not exist, process is not running" else PID=$(cat $PIDFILE) echo "Stopping ..." $CLIEXEC -p $REDISPORT shutdown while [ -x /proc/${ PID} ] do echo "Waiting for Redis to shutdown ..." sleep 1 done echo "Redis stopped" fi ;; *) echo "Please use start or stop as first argument" ;; esac1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.增加执行权限,并启动Redis 。
复制[root@test1 utils]# chmod a+x /etc/init.d/redis #增加执行权限 [root@test1 utils]# service redis start #启动redis1.2.(6)查看redis是否启动
图片
(1)连接redis ,然后添加一些测试参数
复制[root@test1 utils]# redis-cli -h 192.168.1.222 -p 6379 192.168.1.222:6379> set "123" "456" OK1.2.3.(2)编写连接Redis的Lua脚本
复制[root@test1 utils]# vim /usr/local/openresty/nginx/conf/lua/redis.lua local redis = require "resty.redis" local conn = redis.new() conn.connect(conn, 192.168.1.222, 6379) #根据自己情况写ip和端口号 local res = conn:get("123") if res==ngx.null then ngx.say("redis集群中不存在KEY——123") return end ngx.say(res)1.2.3.4.5.6.7.8.9.10.(3)在nginx.conf配置文件中的server下添加以下location
复制[root@test1 utils]# vim /usr/local/openresty/nginx/conf/nginx.conf location /lua_redis { default_type text/plain; content_by_lua_file /usr/local/openresty/nginx/conf/lua/redis.lua; }1.2.3.4.5.随后重新reload配置。源码库
复制[root@test1 utils]# nginx -s reload #重启一下Nginx1.(4)验证Lua访问Redis的正确性
在浏览器输入ip/lua_redis , 如果能看到下图的内容表示Lua可以访问Redis 。
图片
准备工作已经完成,现在要实现OpenResty+Lua+Redis自动封禁并解封IP了。3.4
(1)添加访问控制的Lua脚本(只需要修改Lua脚本中连接Redis的IP和端口即可)
复制ok, err = conn:connect(“192.168.1.222”, 6379)1.注意:如果在Nginx或者OpenResty的上层有用到阿里云的SLB负载均衡的话 ,需要修改一下脚本里的所有…ngx.var.remote_addr,把remote_addr替换成从SLB获取真实IP的字段即可 ,不然获取到的IP全都是阿里云SLB发过来的并且是处理过的IP ,同时,这些IP全都是一个网段的 ,根本没有办法起到封禁的效果)。
完整的Lua脚本如下所示。
复制[root@test1 lua]# vim /usr/local/openresty/nginx/conf/lua/access.lua 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("192.168.1.222", 6379) conn:set_timeout(2000) --超时时间2秒 --如果连接失败 ,跳转到脚本结尾 if not ok then goto FLAG end --查询ip是否被禁止访问 ,如果存在则返回403错误代码 is_block, err = conn:get(BUSINESS.."-BLOCK-"..ngx.var.remote_addr) if is_block == 1 then ngx.exit(403) goto FLAG 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 -- 结束标记 ::FLAG:: local ok, err = conn:close()1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.(2)在需要做访问限制的location里加两段代码即可,这里用刚才的/lua做演示
复制[root@test1 lua]# vim /usr/local/openresty/nginx/conf/nginx.conf1.
图片
主要是添加如下配置。
复制access_by_lua_file /usr/local/openresty/nginx/conf/lua/access.lua;1.其中 ,set $business “lua” 是为了把IP放进Redis的时候标明是哪个location的 ,可以不加这个配置。
随后,重新reload配置 。
复制[root@test1 lua]# nginx -s reload #修改完后重启nginx1.(3)打开浏览器访问192.168.1.222/lua 并一直按F5刷新。
图片
随后 ,连接Redis ,查看IP的访问计数。
复制[root@test1 ~]# redis-cli -h 192.168.1.222 -p 63791.发现redis已经在统计访问lua这个网页ip的访问次数了
图片
这个key的过期时间是30秒,如果30秒没有重复访问20次这个key就会消失 ,所以说正常用户一般不会触发这个封禁的脚本 。
图片
当30秒内访问超过了20次,发现触发脚本了 ,变成了403
图片
再次查看Redis的key,发现多了一个lua-block-192.168.1.158 ,过期时间是300秒,就是说在300秒内这个ip无法继续访问192.168.1.222/lua这个页面了。
图片
过五分钟后再去访问这个页面 ,又可以访问了。
图片
这个脚本的目的很简单 :一个IP如果在30秒内其访问次数达到20次则表明该IP访问频率太快了,因此将该IP封禁5分钟 。同时由于计数的KEY在Redis中的超时时间设置成了30秒 ,所以如果两次访问间隔时间大于30秒将会重新开始计数。
大家也可以将这个脚本优化成 ,第一次封禁5分钟 ,第二次封禁半小时 ,第三次封禁半天,第四次封禁三天,第五次永久封禁等等。