Skip to content

Latest commit

 

History

History
91 lines (65 loc) · 5.82 KB

a_ddos_attack.md

File metadata and controls

91 lines (65 loc) · 5.82 KB

使用redis应对ddos盗刷微信企业支付的经历

这次的文章属于对于之前在工作中发生的一次ddos攻击应对分析和应对经验.很有意思.虽然可能对于大多数的读者和我本人来说这样的事件可能不会在一次发生.但是有一句话说的很好(ps这一句话是我经常看的一个公众号架构师之路里面写的,给大佬的公众号打一个广告).

思路比结论重要,有收获就是好的。

事件

这次是我们公司开发的一个微信公众号,里面有一个答题抢红包的模块.需要用户答题成功后,领取红包,按照设定的代码是根据设置,每天最多拿一个.但是某一天,商务那边拿了微信企业钱包的纪录过来说被别人盗刷了,在短时间内同一个用户密集的领取红包.

然后开始查询日志,确实在凌晨,有用户使用同一个ip密集的访问领取红包的接口,并顺利躲过每日领取一个的检查.

分析

如果当时ddos的攻击的话,可以购买对应的云服务进行防御.但是代码里面肯定还是存在问题.

我个人还怀疑,这些黑客,只是顺手进行尝试盗刷.当然这些也只是猜测

并不准备放上原始的代码.只是大体描述一下代码的逻辑

  • 首先是数据库.这里有一张领取记录表,大概是字段是用户id,领取金额,领取时间

然后发放红包代码逻辑的伪代码是这样的

$count = $this->countpackactive( ['member_id' => $memberid, '@date_format(createtime,"%Y-%m-%d")'=>date('Y-m-d')]);//获取当前用户当日获取的次数
 if($packactiveinfo['pack_membernum'] - $count == 0){
         $this->dataerror('今日答题次数已用完!');
 }

 ...
 ...

 //调用企业支付接口.
 $this->addquestionmember_datetime (['quest_tradenumber'=>$tradenumber,'status'=>1]);//insert领取表,增加当前用户的当日的领取次数

...

上面的代码的思路还是蛮清晰的.但是在并发测试下就出现了盗刷的问题.原本的检查完全失败.那么问题出在哪里?我们在简化上面的代码 在 我们看到这么一个逻辑先检查question_member表,在插入question_member.用一定的数据库经验的朋友可以看出来来这里出现了资源的占用的问题.在第一次访问question_member插入完成之前,第二个接口访问到来,要求检查当前question_member的情况.

我们看看具体发生了什么.首先我查看了当前的事务等级是RR(可重复读),关于事务的隔离等级我推荐查看这篇文章,当然也可以直接阅读官方的文档.在当前的隔离等级下就不会出现脏读幻读的情况下.但是在在高并发的情况下,在第一次查询数据发生后,在插入还没有发生前,如果接口被访问,就会出现,检查数目还是原来的值,而重复插入.实际上就构成了对同一张的资源占用的情况.

资源的占用与并发访问的一致性

这里其实并不像夸夸奇谈,我先说的是这一类的问题其实在计算机的各个层面的都是通用的,不仅仅是数据库中,在操作系统中也见到过注入资源的强占问题,在网络层中出现的一致性问题.而这些问题的解决思路往往很详尽.

加锁

而在这里的方案是增加一个锁,在增加当前用户日领取记录时候使用锁,而在读取时候必须等待之前的记录插入完毕才能读取.为了避免对于采用表锁,使用对用户同一天的领取红包记录生成一个表,记录每天用户领取红包记录.人工添加行级别的锁,来保护数据.关于锁的话题,我觉得这一篇文章中提到几篇文章很合适 大概的伪代码如下

对当前的记录加锁
$count = $db->get('question_member_get', 'time', $where);
if ($count == 0) {
.....
$db->update('question_member_get',$set);

.....
}
对当前的记录解开锁

上面的是第一套方案,当然很快被领导否定了.技术主管提出现在的行业中不流行使用锁(我对于这一点抱有疑问)

redis检查

然后提出第二套方案,使用redis进行每天的日访问记录进行检查.大概的逻辑是这样的


$count = $redis->get( 'question_member:' . $time . ':' $memberid);//获取当前的访问数目
if($packactiveinfo['pack_membernum'] - $count == 0){
        $this->dataerror('今日答题次数已用完!');
...
...
$time = 24*3600;
$redis->set('question_member:' . $time . ':' $memberid, $count+1, $time);
发放红包,如果失败回滚;

这里的使用逻辑是通过redis原子访问特性(redis是原子化,必须等一个访问结束后才有第二个发文),速度快的特性避免了高并发下出现的资源占用问题. 其实这里还是会出现资源的占用,只不过因为redis的响应特别快,从而不会轻易出现,在一次插入结束前,继续访问接口.(平心而论还是有地雷的,因为在实际的测试中,依旧会出现只允许一次抢购,但是出现了两次抢购的情况,应该是几遍高并发的redis,依旧会出现这样的报错).实际上在该项目中也就没有继续优化

使用redis事务检查

这两天在家查了一下redis的文档,果然找到了一组可以使用的命令,那就是使用redis的watch命令配合multi事务进行处理. 我们看看这篇文章,我们通过watch命令就可以是实现一个乐观锁,用于对于用户当天的领取红包加上一个乐观锁,实现check-and-set 操作,和我们上文说的一下,最终还是利用了锁机制,但是因为是在redis这个级别下实现,所以要比mysql要高效率的多.

经验

在这次的事件后我进行了总结

  • 对于关键性质的代码,尤其是涉及到金钱的代码要慎之又慎
  • 在测试,可以借助ab之类的工具,检查这一类关键接口是否会在高并发访问下出现问题