验证码接口上线三天就被刷了,我后来加的几道防线

上个月给一个小项目加了手机验证码注册。接口写好,本地测通,部署上去,前两天没什么事。第三天早上打开发送记录,四百多条,全是连号手机号,间隔不到一秒。

余额直接掉了二十多块。

这才反应过来------验证码接口天生就是公网暴露的,你总不能让用户先登录再注册。攻击者只要知道接口地址,剩下的就是一个 for 循环的事。

先看清楚对方怎么打的

翻请求日志,攻击模式特别朴素:

plaintext 复制代码
2026-07-18 03:14:22 POST /api/sms/send 138****0001 200 12ms 45.77.xx.xx
2026-07-18 03:14:22 POST /api/sms/send 138****0002 200 11ms 45.77.xx.xx
2026-07-18 03:14:23 POST /api/sms/send 138****0003 200 13ms 45.77.xx.xx
...

同一个 IP,User-Agent 是 python-requests/2.31.0,连伪装都懒得做。四百多条请求集中在凌晨三点到四点之间,那会儿谁也没在看。

不复杂,但有效------短信是按条扣费的,刷一晚上就是真金白银。

第一道:按手机号和 IP 做频率限制

最先补的是最基本的限流。两个维度同时卡:

手机号维度:同一个号 60 秒内只能请求一次,24 小时累计不超过 5 次。

python 复制代码
from collections import defaultdict
import time

phone_records = defaultdict(list)

def check_phone_limit(phone: str) -> bool:
    now = time.time()
    # 只保留 24 小时内的记录
    phone_records[phone] = [t for t in phone_records[phone] if now - t < 86400]
    records = phone_records[phone]

    if records and now - records[-1] < 60:
        return False
    if len(records) >= 5:
        return False

    records.append(now)
    return True

IP 维度:单 IP 每分钟最多 10 次,每小时最多 30 次。

如果你用 Redis,可以用 INCR + EXPIRE 来做,比内存字典靠谱:

python 复制代码
import redis

r = redis.Redis()

def check_ip_limit(ip: str) -> bool:
    minute_key = f"sms:ip:{ip}:min"
    hour_key = f"sms:ip:{ip}:hour"

    if r.incr(minute_key) == 1:
        r.expire(minute_key, 60)
    if int(r.get(minute_key) or 0) > 10:
        return False

    if r.incr(hour_key) == 1:
        r.expire(hour_key, 3600)
    if int(r.get(hour_key) or 0) > 30:
        return False

    return True

加上之后,那种单 IP 循环刷号的脚本直接就拦住了。但过了没几天又来一波------这次用了代理池,每条请求的 IP 都不一样,IP 限流没用了。

第二道:人机验证前置

代理池可以换 IP,但过人机验证就费劲了。在前端加一道滑块验证(或者点选、旋转,什么都行),通过之后拿到一个一次性 token,连同手机号一起提交给后端。

javascript 复制代码
async function sendCode(phone) {
  // 用户先通过滑块验证
  const token = await captcha.verify();

  const res = await fetch('/api/sms/send', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ phone, captcha_token: token })
  });
  return res.json();
}

后端拿到 captcha_token 之后必须到验证服务的服务端接口去校验 ,不能在前端 JS 里判断。我见过有人把校验逻辑写在前端------if (result.success) 直接放行------等于没加。

python 复制代码
import requests

def verify_captcha(token: str) -> bool:
    resp = requests.post('https://captcha-provider.com/verify', json={
        'secret': CAPTCHA_SECRET,
        'token': token
    })
    return resp.json().get('success', False)

加了人机验证之后,短信消耗立刻降回正常水平。多数脚本到这一步就放弃了------自动过滑块的成本远高于刷你几条短信的收益。

第三道:服务端 IP 白名单

人机验证拦住了从浏览器方向来的攻击,但如果有人拿到了你的短信服务凭证(模板编码或者 API Key),直接从自己的服务器调短信通道怎么办?

这时候 IP 白名单就有用了。把你业务服务器的出口 IP 配到短信服务的白名单里,只有从这些 IP 发出的请求才能调通短信接口:

bash 复制代码
# 查服务器出口 IP
curl -s ifconfig.me
# 120.78.xxx.xxx

把这个 IP 填到短信服务后台的白名单配置里。如果有多台服务器或者经过了负载均衡,把所有出口 IP 都加上。

这一层的意义在于:就算凭证泄露了,攻击者拿去也调不通。而如果你发现凭证可能泄露,在平台侧重置一下编码,旧的立刻失效。

一个容易踩的坑:重发逻辑

限流做完之后还有一个细节。用户点了发送,等了 30 秒没收到(运营商慢或者网络抖动),又点了重发。如果直接生成一条新验证码发出去,用户手机上会收到两条不同的码,不知道该输哪个。

改成这样:60 秒内重发时不生成新码,把上一条原样再发一次。验证码有效期统一 5 分钟,过期作废。

python 复制代码
code_store = {}

def get_or_create_code(phone: str) -> str:
    now = time.time()
    if phone in code_store:
        code, ts = code_store[phone]
        if now - ts < 300:
            return code

    new_code = f"{random.randint(100000, 999999)}"
    code_store[phone] = (new_code, now)
    return new_code

这样用户不管点几次重发,拿到的都是同一个码,不会混乱。

别忘了加个异常报警

防护做了几层也不能完全放心。加两个最简单的报警:

  • 每小时短信发送量超过日常均值 3 倍,告警
  • 单 IP 一分钟内请求验证码超过 20 次,告警并临时封禁

不需要专门搭监控系统,一个定时查日志的脚本就够。关键是出了问题能第一时间知道,别等月底看账单才发现。

如果你的业务服务器本身就接了告警通道,直接在限流触发时发一条通知就行。我用的是一个 HTTP 接口推消息的服务,限流被触发的时候 POST 一下就能收到通知,不用额外搭东西。

防护层级总结

把上面几道防线列一下:

层级 手段 拦什么
前端 人机验证(滑块/点选) 脚本自动化请求
接口 手机号 + IP 频率限制 高频刷号
通道 IP 白名单 凭证泄露后的非法调用
运营 发送量异常报警 漏网的异常流量

这几层没有哪个是多高深的东西,但不做的话后果很实在------短信通道按条计费,被刷一晚上可能就是好几百块。上线前花半天把这些加好,比出了事再补划算得多。

我自己用的短信服务自带 IP 白名单和凭证重置,不需要企业资质就能接入,模板也是平台提供的现成的不用审核。有需要的话可以看看:push.spug.cc

相关推荐
跨境小彭2 小时前
店群运营实操复盘:批量活动申报自动化优化方案
大数据·运维·人工智能·自动化·跨境电商·temu·temu电商运营
Jlzn88882 小时前
2026年锂电CCS组装线:车规储能双场景兼容自动化方案的技术架构与实践
运维·架构·自动化
AC赳赳老秦2 小时前
语义采集进阶实战:利用 OpenClaw AI 语义识别自动提取网页核心信息,无需手动编写选择器
java·运维·服务器·python·信息可视化·deepseek·openclaw
代码方舟3 小时前
企业级对公银行 KYC 架构:基于天远人脸身份证比对A构建自动化客户尽调网关
运维·人工智能·架构·自动化
ShiXZ2133 小时前
网络调试四剑客:ping / telnet / nc / netstat 速查指令集
运维·开发语言·网络·php
小五传输3 小时前
Serv-U替代方案怎么选?政务机关文件安全传输对比与迁移指南
大数据·运维·安全
woniu_buhui_fei4 小时前
日常工作常用的命令(Linux+JDK)
linux·运维·服务器
snow@li4 小时前
Telnet 全景教程:全平台安装、命令使用、故障排查与安全规范
运维·网络
风翼靓崽4 小时前
记一次使用snap 安装dive后,docker镜像和容器都看不到
运维·docker·容器