自从某公司使用短信验证码验证用户真实性以来,短信便逐步成了公司业务的标配。现在几近每家公司的服务都包括了短信发送这1功能。而用户要求短信时,1般还未注册,所以这个接口是匿名接口(不需要登录)。
因而,坏蛋们就开始捣蛋了。他们通过对软件抓包,得到用户的要求消息,然后摹拟用户对服务器疯狂的发送这1要求,消耗公司的短信资源,骚扰无辜手机用户,乃至造成短信通道梗塞,没法发出正常消息。弄不好还被那些被骚扰者的投诉,封掉公司的短信通道,或被各手机厂商辨认为垃圾短信从而失去营销效果,乃至影响正常业务。
今天,我就遇到这么个捣蛋鬼。他的手里有大约上千台肉鸡(科普1下:“肉鸡”是指那些可以被黑客操控的无辜者的电脑,这些电脑用户其实不知情。之所以可以被操控,多是由于安装了有木马程序的软件,或系统存在漏洞被攻破),因而摹拟消息从全国遮天蔽日而来,服务器日志疯狂刷屏。因而,我们立刻着手处理这件事情。
我们想到了两种策略:
两种方案各有好处,如果访问量不大的话,任选其1便可,如果访问量极高,就需要酌情斟酌了。
我们采取的策略是第2种:
刚开始我们也使用了第1种,将这些IP直接加入到iptables中去,从内核层面封掉这些IP,只是我们觉得解封起来比较麻烦,而且没法与我们的软件集成,特别是很难实现逻辑集成(区分接口,允许1分钟1次,3次以上才封IP),遂使用nginx+redis的方案,毕竟lua程序可以写成我想要的逻辑。
假设我们发送短信的接口是 /sendSms
在nginx.conf文件的 server
段内新建1个location
块,使其匹配正则表达式 ~* sendSms
,然后开始写代码吧:
location ~* sendSms {
default_type 'application/json; charset=UTF⑻';
lua_need_request_body on;
access_by_lua '
-- 为了lua代码可以语法高亮,这里的内容放在后1个代码块,请自行复制到此处便可
';
proxy_pass http://app_backend;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass_header Origin;
}
下面的lua代码请复制到上文单引号内:
-- ngx.exit(ngx.OK)
local cjson = require "cjson"
local ip = ngx.var.remote_addr
if ngx.var.http_user_agent ~= nil or ngx.var.http_user_agent == "" then
local agent = string.lower(ngx.var.http_user_agent)
-- ngx.say("user agent:", ngx.var.http_user_agent)
-- ngx.exit(200)
local sIdx = string.find(agent, "httpclient") or string.find(agent, "java")
-- ngx.say("sIdx:", sIdx)
if (sIdx ~= nil) then
ngx.status = ngx.HTTP_FORBIDDEN
local msg = "你把硬盘拿过来,我直接把数据库给你拷贝1份吧,这样太慢了,我都急死了"
ngx.say(cjson.encode({code=16, msg=msg, R=cjson.null}))
ngx.exit(ngx.status)
return
end
end
local redis = require "resty.redis"
local red = redis.new()
red:set_timeout(1000)
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR
ngx.say("failed to connect: ", err)
return
end
local forbidden, err1 = red:sismember("fbdIP", ip)
-- ngx.say("forbidden:", forbidden)
if forbidden == 1 then
ngx.status = ngx.HTTP_FORBIDDEN
local msg = "你把硬盘拿过来,我直接把数据库给你拷贝1份吧,这样太慢了,我都急死了"
ngx.say(cjson.encode({code=16, msg=msg, R=cjson.null}))
ngx.exit(ngx.status)
return
end
local key = "ip::" .. ip
-- ngx.say("key:", key)
local ttl, err1 = red:ttl(key)
if ttl == -1 then
red:del(key)
end
local res, err = red:get(key)
red:incr(key)
if (not res) or (res == ngx.null) then
--[[
local msg = "failed to get cache"
ngx.say(cjson.encode({code=16, msg=msg, R=cjson.null}))
--]]
red:expire(key, 55) -- 55秒内不允许同1IP超过30次访问
ngx.exit(ngx.OK)
elseif tonumber(res) < 1 then
ngx.exit(ngx.OK)
elseif tonumber(res) >= 1 then
ngx.status = 200
-- ngx.say("redis result is string 1")
-- local msg = "我们认为你有歹意要求的嫌疑,请不要使用及其程序进行访问"
local msg = "慢点,无影手很多累啊"
ngx.say(cjson.encode({code=16, msg=msg, R=cjson.null}))
if tonumber(res) > 10 then
red:sadd("fbdIP", ip)
end
ngx.exit(ngx.status)
return
end
解释
get()
,还使用了 ttl()
,是由于redis的过期回收策略使用的是1种近似LRU算法,致使1定几率的不删除,所以使用ttl进行检查。在大家读到这篇文章的时候,我要顺便说1下这个方案适用的条件。如果没有这些条件,那这个方案对你就不可用,固然,解决思路或许可以有些帮助,如果你善于动手的话,很快也能弄好自己的解决方法。我们的系统满足以下几个条件:
由于之前redis有个可以拿到root权限的漏洞,所以:
redis 的key千万不能被污染,否则正经常使用户的IP会被误伤封禁。
在解决这个问题的时候,我们也是用废了很多脑细胞的,为了节省你的脑细胞,我就免费让你看看。
我的办法是:
通过将日志内所有对/sendSms
接口的调用IP进行统计,找到那些调用次数比较多的,比如大于10次的。用这个命令就行了:
grep "POST /sendSms" logs/ikuaiyue.log | awk '{print $6}' | sed s/IP:// | sort | uniq -c | awk '{print $1 "\t" $2}' | sort -n
然后就会看到这样的结果:
1 115.205.13.179
2 117.136.40.20
2 117.136.94.44
2 117.59.39.22
122 223.104.10.28
第1列是此IP的调用次数,第2列你懂。
好了,现在知道改怎样办了吧?
顺便说1下,我们的日志是这个模样滴:
[2016-05-30 01:25:20.451] [INFO] normal - IP:117.174.26.32 POST /sendSms
[2016-05-30 01:26:17.918] [INFO] normal - IP:117.174.26.32 POST /sendSms
...
略微解释下上面的命令:
grep "POST /sendSms" logs/ikuaiyue.log
| awk '{print $6}' #按空格分割后的第6列(即IP:117.174.26.32)
| sed s/IP:// #删除字符"IP:"
| sort #排序
| uniq -c #去重,并记下重复数,相当与做了个统计操作
#此时重复数字为第1列,IP被放在了第2列.
#但此时格式上有个问题:首列数字是右对齐的
| awk '{print $1 "\t" $2}' #为了消除其右对齐,重新打印以便,并以tab分隔
| sort -n #以首列为根据排序。-n表示当作数字来排列,默许是当作字符串的
把刚刚那个命令改改,只输出最后3000行(具体数字看你的业务繁忙程度了)用做统计:
tail -n3000 logs/ikuaiyue.log | grep "POST /sendSms" | awk '{print $6}' | sed s/IP:// | sort | uniq -c | awk '{print $1 "\t" $2}' | sort -n
上一篇 设计模式06_装饰者模式
下一篇 XML学习1:XML概述