短信验证码接口防刷设计:从限频到双因子令牌的完整方案

发布时间:2026/10/13 17:18:26

短信验证码接口防刷设计:从限频到双因子令牌的完整方案 短信验证码接口防刷设计这个题目我在不同团队里前前后后折腾过好几轮了。老实说每次以为把防护做到位了没过多久总会被新的刷法打脸。短信通道不像别的业务接口它直接和成本、用户体验、账号安全三件事挂钩被刷爆一次损失的不仅是几万条短信费还有用户对产品的信任。这篇文章我不打算给你讲一堆纸上谈兵的概念而是把我自己踩过的坑、验证过有效的方案、以及那些看着合理实则没用的“伪防护”都拆开来讲。尽量让刚接触这块的开发者能直接照着搭一套也让有经验的同行看看有没有可以互相补充的地方。1. 短信接口被刷的常见路径先搞清攻击者到底在打什么在做任何防护之前先得搞清楚一个核心问题攻击者刷你的短信接口图的是什么1.1 三种典型的刷接口动机第一种是恶意消耗成本。这是最无脑也最常见的。攻击者拿到你的接口地址后用脚本批量调用短时间内把手机号段里的号码都遍历一遍。一次攻击可能消耗你几万甚至几十万条短信费用。这种攻击不挑业务只要有暴露的发送接口就能打。第二种是撞库和批量注册。攻击者手里有从其他平台泄露的账号密码库拿你的短信接口当验证工具。如果你的接口在发送验证码之前不校验手机号是否已注册、不校验当前请求是否来自真实用户那攻击者就能通过“尝试发送验证码→观察是否发送成功”来判断某个手机号在你的平台上是否存在为后续的撞库攻击做信息收集。第三种是骚扰特定用户。这个比较恶意攻击者盯上某个具体手机号反复触发发送验证码让受害者的手机被短信轰炸。这种攻击的检测难度更高因为从接口层面看这些请求可能都来自不同的IP、不同的设备指纹频次也不一定很高。1.2 最容易被打的薄弱点都在哪里我自己复盘的结论是大部分被刷的接口都死在这几个地方接口没有做任何前置校验只要拿到地址就能直接调。只防了手机号维度没防IP维度。攻击者用代理池分散请求来源手机号维度根本看不出异常。频控逻辑写在应用层而且用的是非原子操作。并发一高判断就失效了。验证码前置环节缺失用户不需要经过任何“我是真人”的证明就能触发短信发送。日志不完整事后想溯源攻击来源发现连IP都没有记录。这些薄弱点不是单一存在的通常是一起出现。所以防护方案也必须是一整套组合拳而不是加一个限流就完事。2. 第一道防线手机号与IP的滑动窗口限频限频是所有防刷方案的基础但限频怎么做差别非常大。2.1 为什么固定窗口限频不靠谱很多团队一开始用的是这种方案以手机号为维度存储一个计数器比如“1分钟内最多发1条1小时最多发5条”。到期清零。这里有个经典的并发穿透问题假设你设了1分钟最多发1条攻击者在第59秒和第61秒各发一次因为窗口已经切换了两次请求都会被放行。如果攻击者把请求分布在每次窗口切换的临界点那实际发送量会翻倍。更致命的是计数器的原子性。如果用先读再写的方式更新计数在高并发下会出现多个请求同时读到旧值、同时通过校验的情况。这就是为什么很多人明明配了限频压测一打就穿。2.2 用Redis ZSET实现真正的滑动窗口我推荐用Redis的ZSET结构来做滑动窗口限频核心逻辑是把每次请求的时间戳作为score存入ZSET查询时只统计当前时间往前推窗口长度内有多少个时间戳。-- ratio_limit.lua -- KEYS[1]: 频控键例如 sms:freq:mobile:13800138000 -- KEYS[2]: 当前时间戳毫秒 -- KEYS[3]: 窗口大小毫秒例如60000 -- KEYS[4]: 窗口内最大次数例如1 -- 返回1表示允许发送返回0表示拦截 local key KEYS[1] local now tonumber(KEYS[2]) local window tonumber(KEYS[3]) local max_count tonumber(KEYS[4]) -- 移除窗口外的记录 redis.call(ZREMRANGEBYSCORE, key, 0, now - window) -- 统计窗口内记录数 local current redis.call(ZCARD, key) if current max_count then redis.call(ZADD, key, now, now .. - .. math.random(1000000)) redis.call(PEXPIRE, key, window) return 1 end return 0这段脚本用Lua写在Redis里执行保证判断和写入是原子操作高并发下不会出现穿透。手机号维度的频控建议这样设置同一手机号1分钟最多1条1小时最多5条24小时最多10条。同一IP在无登录态下1小时最多10条。注意IP维度的限制要比手机号宽松因为正常用户的出口IP可能是共享的。2.3 分布式环境下频控键的设计细节频控键的设计直接影响到限频的粒度。我见过有人直接用手机号做key结果同一个手机号在不同业务场景下的发送被互相干扰。更合理的做法是把业务场景和手机号一起作为维度。所谓业务场景就是验证码的类型比如注册、登录、找回密码、修改手机号绑定等。不同场景的频控策略是可以不一样的注册场景可以严格一些登录场景要考虑用户被挤下线后重新登录的需求。频控键我推荐这样设计sms:freq:{scene}:{mobile}同时保留一个sms:freq:{scene}:{ip}:{mobile}级别的键用来限制“同一IP对同一手机号”的请求频率。攻击者即使有一整个代理池只要目标手机号是固定的这个维度也能限制住。2.4 纯粹依赖限频的局限这里必须说实话仅靠限频远远不够。攻击者用几百个手机号、几百个IP同时打每个手机号都保持在“1分钟1条”的频控红线内整体请求量仍然能把你的短信通道打满。限频的意义在于提高攻击成本、争取响应时间而不是一劳永逸。真正把绝大多数自动化脚本挡在门外的是下面要讲的验证码升级和设备指纹。3. 第二道防线行为设备指纹与验证码升级这部分是防刷设计的核心战场。如果说限频是“拦速率”那么这一层就是“辨真伪”。3.1 前端埋点与设备指纹的采集逻辑设备指纹的核心思路是在用户请求发送验证码之前前端先采集设备信息生成一个很难伪造的标识后端用这个标识和历史行为做比对。采集的信息不是越多越好关键是你得有办法验证这些信息的一致性。我从实际经验里推荐采集这几类HTTP头部的完整UA信息包括操作系统、浏览器版本、语言、时区偏移。Canvas指纹通过让浏览器绘制特定图形因为不同显卡驱动、不同字体渲染会产生微小差异。WebGL渲染信息这个和设备硬件有强关联。屏幕分辨率和可用分辨率的组合。时区、语言列表、触控支持情况。这些信息会通过前端JS被压缩成一段加密字符串作为请求参数传给后端。后端拿到后会计算出一个device_id并维护这个设备的首次出现时间、历史请求频次、关联的手机号数量和账号数量。3.2 在什么条件下必须升级为图形验证码设备指纹不是万能的攻击者可以做模拟浏览器指纹也可以使用真实的浏览器环境配合自动化工具。判断标准不能只看设备指纹是否合法还要看组合后的风险等级。我常用的判定逻辑是打分的设备指纹首次出现且直接请求短信接口高风险要求先过滑块或图形验证码。设备指纹在1小时内关联超过2个手机号高风险要求完成拼图验证。设备请求的User-Agent和操作系统语言与手机号归属地明显不匹配风险提升。设备指纹合法且过去7天有稳定的登录记录低风险直接放行。这里要特别说明一下验证码的成本是由普通用户承担的所以设计目标是“让99%的真实用户无感知让1%的可疑用户付出额外操作成本”。如果每个用户每次发验证码都要玩拼图用户流失率会直线上升。3.3 图形验证码的服务端校验细节图形验证码的校验经常被忽略一个点必须一次性使用且和会话ID绑定。不能存在“这个验证码答案5分钟内有效可以反复提交”的情况。另一个容易犯的错是将验证码校验和短信发送分成两个接口但攻击者可以绕过验证码接口直接调用短信接口。正确的做法是把验证码校验结果作为短信发送的前置令牌从流程上强制绑定。3.4 行为轨迹验证码的采集逻辑相对于传统图形验证码行为式验证码比如滑块、点选是目前防自动化效果较好的一类方案。服务端通过校验滑块的轨迹数据判断拖动过程是否符合人类操作特征是否有加速减速、是否有轻微抖动、轨迹点的时间间隔是否符合物理规律。这套方案的问题在于部分老旧的WebView环境不支持以及无障碍用户的操作轨迹经常被误判为机器。所以我的建议是做成降级链路行为验证失败后可以降级为传统数字图片验证码保证功能的可用性。4. 第三道防线双因子令牌机制把发送权与请求权分离这是我认为整个防刷体系里最有价值的一环也是很多团队没做或者没做对的一环。4.1 核心流程重构先换令牌再发短信传统流程是前端点击“获取验证码”→后端直接调用短信服务发送。这个流程最大的问题在于后端无法区分请求是否经过了前置的完整性校验。攻击者不需要关心你的页面逻辑只关心你的短信接口地址只要能成功POST一次短信就发出去了。双因子令牌机制的思路是把“发送短信”这个动作分成两步。第一步客户端先请求一个pre_auth_token。这个请求需要携带设备指纹、行为验证码的校验结果、以及当前的时间戳。服务端校验通过后签发一个短时效的tokentoken绑定本次请求的手机号有效期建议不超过2分钟且只能使用一次。第二步客户端拿着这个token再请求短信发送接口。发送接口拿到token后先校验token是否存在且未被使用、是否与当前请求的手机号一致、是否过期。全部通过后才真正调用短信通道。# 伪代码示例用于说明token生成和校验的流程 import hashlib import time import redis r redis.Redis(host127.0.0.1, port6379, db0) def issue_pre_auth_token(mobile: str, device_id: str, scene: str) - str: payload f{mobile}:{device_id}:{scene}:{time.time()}:{secrets.token_hex(8)} token hashlib.sha256(payload.encode()).hexdigest() # 将token写入Redis有效期120秒绑定手机号和场景 key fsms:pauth:{token} r.hset(key, mapping{ mobile: mobile, device_id: device_id, scene: scene, issued_at: time.time() }) r.expire(key, 120) return token def verify_pre_auth_token(token: str, mobile: str, scene: str) - bool: key fsms:pauth:{token} data r.hgetall(key) if not data: return False if data.get(bmobile).decode() ! mobile: return False if data.get(bscene).decode() ! scene: return False # 校验通过后立即删除token确保一次性使用 r.delete(key) return True4.2 双因子机制到底解决了什么问题双因子机制解决的核心问题是接口身份的不可伪造性。有些团队会问我前端已经做了设备指纹采集后端也校验了为什么还要多这一步答案是如果发送接口本身不做token校验攻击者依然可以直接POST短信接口。
延伸阅读

更多相关文章

2026/10/13 17:18:26

多微网能量互联优化调度:面向低碳经济目标的Matlab建模与实现

三个微网如果各自闷头调度,光伏大发的时候用不完、负荷高峰的时候又得高价买电,这本身就是一种浪费。把它们用联络线连起来,让功率在微网之间流动,再叠加上碳排放约束,就是我这次想聊的核心问题——面向低碳经济运行目…

2026/10/13 17:13:26

开放式代码审查:一种可落地的开源协作范式

1. 这不是代码审查工具,而是一套可落地的开源协作范式“open-code-review”这个标题乍看像某个新出的 GitHub Action 或 CLI 工具,但实际翻遍主流包管理器、GitHub Trending 和 Hacker News 近三个月热帖,都找不到一个叫这个名字的成熟项目。…

2026/10/13 17:13:26

C语言期末考逆袭攻略:高频考点与刷题技巧全解析

每年期末,总有一批同学在C语言这门课上摔跟头。作为计算机类专业的基础课,C语言不仅挂科率常年稳居高位,而且它还是后续数据结构、操作系统等硬核课程的基石。我在某师范类高校读本科时,C语言这门课也差点翻车,后来硬是…

2026/10/13 18:28:53

Kettle 9.3 跨云ETL实战:S3/GCS连接、JDK17适配与Spoon启动避坑指南

简介:本资源为Kettle(Pentaho Data Integration)最新9.3版本的官方安装包百度网盘直链,面向数据工程师、ETL开发人员及大数据初学者,解决国内用户因网络限制难以获取国际开源ETL工具的问题。资源以1个14KB的docx文档形…

2026/10/13 18:28:53

AGENTS.md:给AI编程建一份项目记忆说明书

不跟你绕圈子,这篇文章的出发点是我这段时间反复踩到的一个问题:我把同一个项目背景跟AI编程工具讲了不下十遍,每次新建会话它都像第一天上班一样,对我的代码库一无所知。模型的对话窗口装得下上下文,但它记住的东西在…

2026/10/13 18:28:53

MySQL到HBase实时同步:Maxwell+Kafka+Thrift生产实践

1. 项目概述:为什么用 Maxwell Kafka Thrift 这套组合打通 MySQL 到 HBase 的链路在某数据中台的实际演进过程中,我反复遇到一个高频痛点:业务库 MySQL 里沉淀了大量高价值的实时交易、用户行为、订单状态变更数据,但这些数据天…

2026/10/13 18:28:53

PS2键盘模拟程序:从PS/2协议到单片机固件实现的完整指南

简介:面向单片机与嵌入式开发者,这份PS2键盘模拟程序源码演示了如何用单片机模拟PS/2键盘行为,适用于自动化测试、特殊输入设备等需要注入键盘信号的场景。资源共3个文件,包含1个C源文件与2个头文件,压缩包仅2KB&#…

2026/10/13 18:23:47

微信小程序二手交易源码数据库.zip:从导入到跑通与二次开发

简介:微信小程序二手物品交易源码包,面向计算机专业毕业设计、课程设计及小程序开发实战学习者,提供一套以Java为后端、微信小程序为前端的完整可运行项目。系统实现商品发布、分类浏览、关键词检索、用户登录与订单管理等典型交易流程&#…

2026/10/13 5:19:27

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/13 17:09:18

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/12 12:54:07

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/13 0:04:41

基于Triton部署YOLO目标检测:从训练脚本到线上服务

简介:这份资源面向希望将目标检测模型落地到生产环境的深度学习开发者与算法工程师,聚焦于用 Triton Inference Server 部署 YOLOv9 的完整实战方案,解决从训练权重到线上推理服务之间的工程化难题。压缩包共 16 个文件,约 886KB&…

2026/10/13 0:04:41

手把手教你云端部署Qwen-Image-2.1:从GPU选型到在线文生图服务

Qwen-Image-2.1 这个开源的图像生成模型,最近在内容创作和 AI 应用圈子里热度相当高。它属于通义 Qwen 系列里的视觉生成分支,让我眼前一亮的是它对中文长文本的理解能力——以前用那些老方案生成带中文文字的图,十个里九个是“乱码印刷体”&…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑