短信接口几乎是所有业务系统的标配,注册、登录、找回密码都离不开它。但短信接口同时也是一个非常容易被攻击者盯上的入口:一次短信的成本虽然只有几分钱,可一旦接口被恶意脚本批量调用,一夜之间产生几万条发送记录,损失就是实打实的真金白银。更麻烦的是,攻击者刷的号码往往来自轰炸平台,受害者的投诉还可能直接导致你的短信签名和模板被运营商封禁。这篇文章就来系统聊聊短信接口的防护思路和具体落地方法。

为什么短信接口容易被刷:先弄清攻击手法
想做好防护,得先知道对手怎么打。短信接口被恶意调用,常见手法主要有这么几种。
第一种是直接重放攻击。攻击者用抓包工具拿到你的发送短信请求,然后写个脚本循环调用。如果你的接口没有任何频率限制和签名校验,这种攻击几乎是零成本的。第二种是短信轰炸,攻击者拿到一批受害者手机号,利用你平台的接口给这些号码发送验证码短信,本质上就是把你的接口当成了免费的轰炸工具。第三种是撞库场景下的滥用,攻击者批量尝试手机号,触发送短信接口来探测哪些号码注册过。
这些攻击能成立的前提,通常是接口存在这几个漏洞中的一个或多个:没有图形验证码这种人机校验、IP和手机号维度的限流缺失、请求参数没有签名导致可以随意伪造、前端校验逻辑被绕过。后面的防护手段基本都是针对这些漏洞逐个击破的。
第一道防线:限流策略的正确设计
限流是防护的基石,但很多系统的限流做得太粗糙,只限制了单IP每分钟几次,实际效果很差。真正有效的限流必须是多维度组合的。
首先是IP维度。同一个IP短时间内大量请求发送短信,明显异常,一般限制单IP每分钟不超过5次、每天不超过50次比较稳妥。其次是手机号维度,单个手机号验证码发送间隔要强制60秒以上,一天最多发送10次左右。最后是全局维度,如果你的业务平时每天发1万条短信,某天流量突然涨了十倍,就该触发告警甚至熔断了。
下面给出一个基于Redis的多维度限流示例,使用Java实现,核心思路是用计数器加过期时间:
public boolean checkSmsLimit(String ip, String phone) {
// IP维度:每分钟最多5次
String ipMinKey = "sms:limit:ip:min:" + ip;
Long ipMinCount = redisTemplate.opsForValue().increment(ipMinKey);
if (ipMinCount == 1) {
redisTemplate.expire(ipMinKey, 60, TimeUnit.SECONDS);
}
if (ipMinCount > 5) {
return false;
}
// IP维度:每天最多50次
String ipDayKey = "sms:limit:ip:day:" + ip + ":" + LocalDate.now();
Long ipDayCount = redisTemplate.opsForValue().increment(ipDayKey);
if (ipDayCount == 1) {
redisTemplate.expire(ipDayKey, 1, TimeUnit.DAYS);
}
if (ipDayCount > 50) {
return false;
}
// 手机号维度:60秒内只能发1次
String phoneKey = "sms:limit:phone:" + phone;
Boolean first = redisTemplate.opsForValue().setIfAbsent(phoneKey, "1", 60, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(first)) {
return false;
}
return true;
}
这里有个细节要注意:手机号的60秒间隔限制必须用setIfAbsent这种原子操作实现,而不能先get再set,否则并发请求会同时通过校验。另外,取客户端IP时要小心代理场景,直接取X-Forwarded-For的第一个值是可以被伪造的,建议结合RemoteAddr以及可信代理白名单来综合判断,否则攻击者换个伪造IP头就能绕过IP限流。
第二道防线:图形验证码和行为校验
限流只能控制频率,挡不住分布式的慢速攻击。这时候需要人机校验来确认请求背后是真实的用户。最常见的方案是在发送短信前先要求用户完成图形验证码校验,服务端验证通过后才允许发送短信。
流程上建议这样做:前端先请求一个验证码挑战,用户完成滑动拼图或点选文字后,服务端生成一个一次性token返回给前端;前端带着token去调用短信接口,服务端验证token的有效性并立即作废。这样即使脚本拿到了短信接口地址,没有有效token也发不出去。以滑动验证为例,服务端校验的核心逻辑大致如下:
public boolean verifyCaptcha(String captchaId, String slideTrack) {
// 从Redis中取出验证码记录
String record = redisTemplate.opsForValue().get("captcha:" + captchaId);
if (record == null) {
return false; // 验证码不存在或已过期
}
// 立即删除,保证一次性使用
redisTemplate.delete("captcha:" + captchaId);
// 校验滑动轨迹:真人滑动的轨迹速度是先快后慢,有细微抖动
// 机器生成的轨迹通常是匀速直线,直接可判定为机器人
return TrackAnalyzer.isHumanLike(slideTrack);
}
比传统图形验证码更强的方案是接入专业的行为验证服务,比如极验、网易易盾或者阿里云的验证码产品,它们会综合鼠标轨迹、滑动速度、设备指纹等信息做风险判断。对于中小团队来说,直接接入这类服务的成本不高,防护效果却比自己写的好得多。
还有一个容易被忽略的点:验证码校验和短信发送必须绑定。有些系统图省事,验证码校验通过后token可以重复使用,攻击者只需要人工通过一次验证,就能拿着token无限刷短信。所以token必须严格一次性、短有效期(建议120秒内),并且和目标手机号绑定校验。
第三道防线:接口签名与服务端校验
如果你的短信接口是给App或者小程序用的,那么接口签名是必不可少的。签名的作用是防止请求被篡改和重放,即使攻击者抓到了请求数据,也无法伪造合法的新请求。
签名的基本做法是:客户端把所有请求参数按key排序后拼接,再拼上一个双方约定的密钥,计算MD5或HMAC摘要作为签名参数一起发送。服务端用同样的方式重新计算并比对。为了防重放,还要加上时间戳和随机数,服务端拒绝时间戳偏差超过5分钟的请求,并记录已使用的随机数防止重复提交。
public boolean verifySign(Map<String, String> params, String sign) {
// 剔除签名字段本身
params.remove("sign");
// 参数按key的字典序拼接
StringBuilder sb = new StringBuilder();
new TreeMap<>(params).forEach((k, v) -> sb.append(k).append("=").append(v).append("&"));
sb.append("secret=").append(SECRET_KEY);
String expected = DigestUtils.md5Hex(sb.toString());
// 时间戳校验,允许5分钟偏差
long ts = Long.parseLong(params.get("timestamp"));
if (Math.abs(System.currentTimeMillis() / 1000 - ts) > 300) {
return false;
}
return expected.equals(sign);
}
此外还要强调一点:所有校验逻辑必须在服务端完成。有些开发者把手机号格式校验、发送频率判断写在客户端,认为这样就够了,这是典型误区。客户端代码是完全可以被绕过的,脚本直接构造HTTP请求就能跳过一切前端逻辑。前端校验只能作为体验优化,安全校验永远要以服务端为准。
监控告警与降级兜底策略
上面三道防线做到了,被刷的概率已经很低,但安全没有绝对,监控告警是最后的保险丝。建议对这几个指标做实时监控:短信发送总量按小时环比、单IP发送次数TOP排名、失败率、同一手机号段的集中发送行为。一旦短信发送量偏离基线超过一定阈值,比如10分钟内发送量超过日常峰值的3倍,立即触发告警并自动进入降级模式。
降级模式可以这样设计:异常时自动收紧限流阈值、强制开启更严格的人机验证、暂停可疑号码段的发送,甚至直接熔断短信服务一段时间。宁可让真实用户暂时发不出短信,也不要让损失持续扩大。下面是一个简单的熔断判断逻辑:
public boolean shouldCircuitBreak() {
// 最近10分钟的发送量
long recent = counter.get("sms:send:recent10min");
// 日常10分钟基线值
long baseline = counter.get("sms:send:baseline10min");
// 超过基线3倍则熔断
if (recent > baseline * 3) {
alertService.notify("短信发送量异常,触发熔断");
return true;
}
return false;
}
最后还有一个实用建议:给短信账户设置余额告警和日发送量上限。主流短信平台都支持这两个功能,设置之后即使所有防线都被突破,损失也会被控制在一个可接受的范围内。同时在业务层面,验证码的有效期设为5到10分钟即可,过期作废;同一天内同一手机号的验证码错误尝试次数也要限制,防止验证码被暴力猜解。
总结
短信接口防护不是单点技术,而是一套组合拳:多维度限流控制频率,人机验证拦截脚本,签名机制防止伪造和重放,监控告警兜底止损。这四层防线层层递进,缺一不可。如果预算允许,行为验证服务和第三方风控可以直接接入,能省不少自研成本;如果资源有限,优先把限流和图形验证码这两件事做扎实,就能挡住绝大多数的恶意调用。记住一个原则:任何依赖客户端的校验都是不安全的,真正的防线永远在服务端。