如下是用户页面交互。输入手机号,即可获取验证码。用户体验方面已经超级简单了。

不过,简单是要有成本的。安全控制方面,程序员得琢磨。

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。

短信接口防恶意攻击策略,使用User-Agent防止HttpClient发送http请求时403 Forbidden和安全拦截 Safe 第1张

 

在系统安全、信息安全、系统安全防御领域,短信盗刷是老生常谈的话题了。我们公司的系统也经历过至少3次盗刷。每次动辄损失2万~5万条的短信。 

近几年,随着qq授权登录、微信授权登录等登录方式的流行,短信盗刷的情况似乎是少了。不过,互联网企业总是习惯要留下用户的手机号的,毕竟这么做非常利于流量获取。

 

短信验证码登陆,通常的做法是图形验证码。简单实现的话,就是 当用户输入的手机号发生变化时,页面异步请求服务端生成图形验证码的接口,服务端返回图片文件流,页面生成验证码图片。用户输入验证码,然后请求服务端获取验证码的接口。服务端会校验用户输入的验证码是否正确,正确了才会发送短信验证码。

因为图形验证码是通过文件流传输的,所以很难破解。当然,倒是有识别图片的工具,不管怎么说,还是有一定难度的。不识别图片呢?随机生成4位验证码,用撞库的方式来恶搞?显然,命中的几率也很小。就是说,用图形正麻烦的方式,恶意攻击的难度比较大。 我们看12306或其他的互联网网站,动不动让选特定的图形,或滑动拼图,或依次选特定的文字,这种安全性都是相当高的。

据说,阿里的招数更绝!可以记录鼠标在页面的轨迹,进而识别出来是人在操作,而非机器模拟。

 

所谓安全,安防,说白了,是防君子不防小人的,道高一尺魔高一丈。我们只能做到更安全一些,最大程度减少恶意攻击导致的短信资源浪费。没办法做到100%最安全。

 

言归正传!

我们这种需求是一个乘客注册/登陆的页面。乘客输入手机号,然后点击获取验证码,系统会判断,如果是新用户,或用户状态正常,就会发送短信验证码。考虑到较好的用户体验,没加图形验证码。

这种简洁的操作,如果被非正常的用户利用,那可就麻烦了。那么,如何最大程度规避短信盗刷呢?

 

我们先分析一下非正常的场景:

┣  短信接口泄露出去了。日常办公大家疏于信息传递,导致接口泄露。

┣  接口在网络上被截获。

┣  短信服务商作祟。不排除这种情况呵~

以上情况,短信接口如果是裸奔的,就会被当做小白鼠为人所恶搞。

 

裸奔的短信验证码接口:

GET /api/sendSmsCode?phone=*** HTTP/1.1
Host a.b.com

只要拼一个类似于 a.b.com/api/sendSmsCode?phone=18812345678 的url就可以触发一条短信。恶搞这种小白鼠接口,是不是很刺激?

 

接下来,我们对这个接口来做安全控制。

【首先】必备的参数校验不可少

0.1 手机号合法性校验。不能为空 11位 以1开头 校验前两位或前三位号段,比如13、15、18、131/2、152、183/6/8/9...(可选,稍有不慎,就有可能会过滤掉正常的号码)。过滤特殊号码,比如88888888、11111111、22222222、12345678、38383838...

 

【其次】我们来分析正常的浏览器请求:

▼Request Headers:
POST /api/passenger/sendSms HTTP/1.1
Host: che.shenbianhui.cn
Connection: keep-alive
Content-Length: 43
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Content-Type: application/json;charset=UTF-8
Origin: http://che.shenbianhui.cn
Referer: http://che.shenbianhui.cn/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: pgv_pvi=2428115968; UM_distinctid=170257b17e51b3-01ea5235ff274e-b383f66-e1000-170257b17e6177; Hm_lvt_cb56ec9ce26d8a82ead7aa15af69e6e0=1581176790,1581304635,1581499290; Hm_lvt_29c6c62e8f0bd1061bcc0e3cb6b3d53d=1583891251,1585192233,1586140252
▼Request Payload:
{"phone":"17813270522","userType":"driver"}

  

1.1. 方法用POST请求

1.2. 判断请求头参数。服务端只接收正常浏览器请求。

1.2.1 校验User-Agent。使用User-Agent防止HttpClient发送http请求时403 Forbidden和安全拦截

1.2.2 校验Reffer

         短信接口防恶意攻击策略,使用User-Agent防止HttpClient发送http请求时403 Forbidden和安全拦截 Safe 第2张

 

 至于点对点的攻击,也可以伪造User-Agent、Reffer的值,伪装成正常的浏览器请求。所以,这远远不够。继续往下看。

 

【再次】请求次数限制

分布式系统直接利用redis的incby来实现计数即可。

2.1 增加IP次数限制。B/S型的对外网站,我们无法做IP白名单控制。不过,同一IP,在指定时间段之内,请求次数要做上限控制。比如5分钟之内不超过50次。这要根据业务情况来评估。

      恶意请求有时会用代理IP,当然,使用代理IP本身是有成本的。

2.2 “一刀切”“限流” 在指定时间段之内,总的请求次数不能超过阈值。比如,5分钟内总请求量不能超过1000次。这要根据业务情况来评估。


【第四】接口参数复杂化

3.1 增加一个key参数,就像支付接口中常见的签名一样。

3.1.1生成key的规则:前后端约定。同时,尽量保证每次请求的key都不同。比如:手机号=18612345678,则key=MD5(手机号前3位186 + 手机号后3位678 + 当前时间/分钟)

       前后端都用这种方式生成key,前端页面通过js脚本生成“签名”,服务端“验签”。

       需要注意的是:时间校验要留buffer,客户机时间与服务器时间并不完全相同。可以用循环或递归算法搞定。

3.2 更靠谱一点的方案。从以上的方案进一步脑洞大开。服务端增加一个生成key的api。一旦用户修改了手机号,就调用api获取一个key,获取验证码的时候同时上送这个key。这样,短验接口每次校验key是否一致就可以了。

      我们通常的保证幂等性的方案,也是生成一个ticket,用户端提交数据的时候,服务端校验ticket,ticket匹配才持久化数据,然后删除ticket;服务端一旦发现ticket不存在,则视为非法请求。

 

综合以上方案的控制,我们就能很大程度保证接口的安全。

只要思想不滑坡,方法总比困难多。

BTW,3.1和3.2的方案,需要前后端配合。当我们找项目组的前端小伙伴讨论时,前端小伙写VUE、NODEJS、JavaScript脚本相当醇熟,他觉得这样做没有什么意义。他打开浏览器的调试工具,说别人一看就知道怎么回事了。这种方案,充其量也就能有1%的改善。我的观点:1.并不是所有的人都知道这个页面的存在;2.并不是所有的人都能找到那段js代码;3.并不是所有的人对前端都很熟。  因为这个页面是他亲自做的,接口是他亲自调用的,所以,他很了解。并不是所有的人都像他一样了解的,包括我们这些后端的程序员。 让我想到一句话:手里有把锤子,看什么都是钉子。 一个人的思维会影响行动。也许,有些技术偏执的人,都多少会有些不羁吧。

扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄