尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

短信验证码接口设计实战:从防刷限流到第三方对接

短信验证码接口设计实战:从防刷限流到第三方对接 做后端开发这几年验证短信接口是我几乎每个业务系统里都会遇到的模块。最近在给库存系统做业务流梳理又配合 AI 电商客服接了一遍短信验证发现很多团队在接入时总在细节上踩坑有的验证码 30 秒就过期用户还没输完就失效有的频率限制没做被刷了一晚上短信欠费几百块有的对接第三方接口超时没兜底用户点了发送按钮半天没反应。这些问题的根源往往不是第三方 SMS 服务商不给力而是应用思路和实现细节没想清楚。这篇文章我打算讲透验证短信接口在业务系统里怎么设计、怎么落地、怎么排障。无论是传统业务后台、库存管理这类内部系统还是 AI 电商客服这类面向终端用户的场景短信验证码的底层逻辑都是相通的生成、存储、发送、校验、限流、风控、日志、监控。我会把每一步的关键选择都拆开讲也会把实际开发中不好查的问题列成速查表。适合刚接手相关需求的后端同学也适合做架构设计时想避免返工的技术负责人。1. 业务场景梳理与方案选型思路1.1 哪些业务场景真的需要短信验证先别急着写代码第一步是把“哪些地方要验证”这件事理清楚。我在实际项目里遇到的情况大致可以分成三类。第一类是用户身份确认类最常见。用户注册、登录、找回密码、修改手机号这些操作都涉及账号安全需要确认当前操作者就是手机号的持有者。这类场景对验证码的安全级别要求高通常要在短信验证的基础上叠加其他风控字段比如设备指纹、IP 归属地、操作频率。第二类是敏感业务操作类。库存系统里的调拨出库、盘点差异调整、高额订单审批、退款审核这些操作往往会改变核心业务数据一旦操作人不是本人或者在非工作时间被异地登录风险很大。在这类内部系统里短信验证码直接嵌进业务流的审批环节和人工审批并行走效果很好。我最近在整理库存系统业务流程图时就把“短信验证”画成了出库单提交后的一个判断节点库存管理员提交调拨单系统先判断是否超过阈值超过则强制要求手机验证码验证通过后才进入下一级审批。第三类是服务交互升级类典型就是 AI 电商客服。用户和机器人对话时如果提出“帮我把收货地址改了”“我要查一下别人的订单”“申请退款到原账户”这时候 AI 需要确认对话者身份。让用户输入账号密码不现实弹人脸识别又太重短信验证码是最平滑的降级方案。AI 客服识别出敏感意图自动下发验证码用户在对话框里填完AI 再拿着校验结果去调用订单系统的接口。搞清楚场景之后你才会知道验证码的时效、长度、重发间隔到底怎么定。内部系统可以接受 10 分钟有效期因为操作人可能去打电话核实面向 C 端用户的注册登录5 分钟比较合适再长会明显增加被盗用的风险窗口。1.2 方案选型自建短信平台还是接入第三方服务商很多团队一上来就纠结这个问题。我直接说结论除非你是运营商背景或者日发送量在百万级以上且对成本极其敏感否则老老实实接第三方短信服务商。自建短信平台意味着你要直连运营商的短信网关比如 CMPP 协议要自己维护通道、做内容审核、处理大量省份路由规则、应付通道故障和黑名单问题。这套东西投入的人力成本远超你想象。我见过一个团队为了省短信费自建网关结果光处理“三网路由不达”的问题就耗费了两个专职运维最后算下来比第三方贵得多。第三方服务商的优势不是“便宜”而是“省心”和“稳定”。阿里云短信、腾讯云短信、容联云这类服务商把运营商通道、签名报备、模板审核、状态报告回推这些都封装好了你只需要调用一个 HTTP 接口。应用系统是 Java 就接官方 SDK是 Go 或 Python 就用自己的 HTTP 客户端发个 POST 请求开发工作量差不了多少。选服务商的时候重点看几个维度到达率尤其是三网覆盖、状态报告发送后是否能异步回传成功/失败、模板审核速度、并发上限和价格。我习惯用表格把这些维度横向比较同一时间拿两个服务商各发 50 条测试短信到不同运营商手机号实测到达速度和内容是否乱码。注意不要只看“发送成功”回执因为短信服务商只会告诉你“请求已受理”真正的到达状态要等状态报告回调这两者之间有延迟一定要把回调链路设计好。另一个容易忽略的点是服务商故障时的逃生通道。我会在配置中心里同时配两个服务商主服务商连续失败超过 3 次就自动切到备用服务商。这个能力看似简单真到活动大促短信量暴涨时能救命。2. 验证短信接口的核心细节拆解2.1 接口协议与参数设计短信验证码接口从设计上看至少包含两个核心接口发送验证码接口和校验验证码接口。有的系统把校验逻辑直接写在业务接口里也可以但参数设计和返回语义必须统一否则前端和后端对不齐后期维护会非常痛苦。发送验证码接口通常这么设计POST /api/sms/code/send Content-Type: application/json { mobile: 13800138000, scene: login, # 业务场景login | register | reset_pwd | stock_audit | ai_cs template_code: SMS_123456, # 第三方模板ID client_ip: x.x.x.x, # 可选网关层自动取 device_id: abc-1234 # 可选端侧设备标识 }关键参数不是短信内容而是scene。同一个手机号在登录、注册、改密场景下验证码必须隔离。比如用户已经在注册流程里拿到一个验证码切到登录页又点了一次如果后端用的是同一个 key后生成的验证码会把前一个覆盖掉导致用户在注册页永远校验失败。解决办法是把 scene 拼进缓存 key比如sms:code:{scene}:{mobile}不同场景互不干扰。template_code也很重要。第三方短信模板里通常会有占位符比如“您的验证码为 ${code}${minutes} 分钟内有效”接口参数要传结构化字段而不是把整条短信内容拼好再传。这样做的好处是内容审核、敏感词过滤、模板匹配都由服务商处理你传过去的是干净的变量。再来看校验验证码接口POST /api/sms/code/verify Content-Type: application/json { mobile: 13800138000, scene: login, code: 482913 }返回结果就三个状态成功、失败验证码错误、过期验证码不存在或已超时。前端拿到结果后给出对应提示。这里有个设计细节校验成功后验证码是立即作废还是保留一段时间我建议立即作废。因为同一验证码如果允许短时间内重复校验会放大盗用风险。实际操作里校验接口把 Redis 里的 key 删掉再返回成功删除和校验要做成原子操作避免并发请求同时校验同一个验证码。用 Redis 的 Lua 脚本或GETDEL指令能很优雅地解决。2.2 验证码的生成、存储与过期策略验证码生成看起来简单其实有几个容易被忽略的细节。首先长度。4 位还是 6 位我建议 6 位纯数字。4 位验证码一共只有 1 万种组合攻击者如果在有效期内暴力穷举每秒试几十次5 分钟内试完所有组合完全有可能。6 位数字是 100 万种组合配合“最多试错 5 次就锁定”的策略安全性就够用了。其次不要用随机数生成函数直接做因为很多语言的rand()是可预测的。建议使用secretsPython、SecureRandomJava或者依赖第三方 SDK 内置的验证码生成工具。验证码本身是安全敏感数据任何可预测性都会变成漏洞。再次存储。我强烈建议把验证码存 Redis不要存数据库。原因有三一是验证码有天然过期语义Redis 的 TTL 正好匹配二是校验时需要删除Redis 删除是 O(1)三是高并发下 Redis 的读写性能足够。缓存 key 的设计我前面提过sms:code:{scene}:{mobile}value 就是验证码和尝试次数可以用 JSON 或者两个 key 分别存。为了省空间我习惯一个 key 存结构体比如{ code: 482913, attempts: 0, last_sent: 1720000000 }过期策略上登录注册类场景我通常设 5 分钟内部审批类场景设 10 分钟。要注意“5 分钟有效”是从发送成功那一刻开始算而不是用户收到短信那一刻开始算。短信通道有时候延迟 30 秒甚至一分多钟如果 TTL 设得太短用户刚收到短信验证码就已经过期了体验非常糟糕。所以我会把有效期设置成“短信预计到达时间 业务操作合理时间”面向 C 端至少留 5 分钟内部系统留 8 到 10 分钟比较稳妥。还有一个细节同一手机号、同一场景下如果用户第二次点击发送旧验证码要不要立即作废我的做法是作废旧验证码并重新开始计时。这样可以避免“两个验证码同时可用”的混乱。在缓存层面第二次发送前先删除旧 key再写入新 key就能保证任何时刻只有一个有效验证码。2.3 频率限制与防刷策略整个短信验证接口里防刷是最容易出问题的一环。短信是按条计费的没有有效限流一台脚本就能把一个月预算打光。我见过的最离谱案例是某系统的发送接口只校验了手机号格式没有设置任何频率限制被刷了一晚上第二天看到账单的时候整个人都懵了。频率限制至少要覆盖三个维度。第一同一手机号发送间隔。比如 60 秒内不能重复发送1 小时内最多发送 5 次24 小时内最多发送 10 次。第一道间隔用 RedisINCREXPIRE实现非常简单。第二同一 IP 发送总量。比如 1 小时不超过 20 次24 小时不超过 50 次。这个逻辑放在网关层做比较合适因为网关能看到真实客户端 IP。第三同一设备标识device_id发送总量。移动端传上来的 device_id 能帮你在 IP 变化时做关联。除了频率限制我还会在前端加一道“人机验证”。用户在点击“获取验证码”之前先完成一个滑块或点选验证码。这套东西自己开发成本不低直接用第三方的人机验证服务就行。它的意义不是完全拦住攻击者而是大幅提高批量操作的成本。攻击者写脚本时处理图形验证的复杂度比单纯调一个 HTTP 接口要高好几个数量级。另外要加黑白名单机制。黑名单里维护被举报过的手机号、命中风控规则的 IP发送前先查黑名单命中直接拒绝。白名单是给测试环境用的测试时可以内置几个白名单手机号不走真实短信通道验证码固定为123456方便前后端联调。但白名单绝不能上线到生产环境我见过有人把测试白名单忘了删导致正式环境里某些手机号可以直接用固定验证码登录这是非常严重的安全事故。3. 实际开发中的落地实现3.1 后端核心代码实现Java 和 Python 我都写过短信验证码模块这里用 Python 伪代码把核心逻辑写出来重点是流程不是某个具体语言。发送验证码的逻辑def send_code(mobile, scene, client_ip, device_id): # 1. 手机号校验 if not is_valid_mobile(mobile): return error(INVALID_MOBILE) # 2. 黑名单校验 if is_blacklisted(mobile, client_ip): return error(BLACKLISTED) # 3. 频率限制同一手机号同一场景 60 秒内不可重复发送 interval_key fsms:interval:{scene}:{mobile} if redis.exists(interval_key): return error(SEND_TOO_FREQUENT) redis.set(interval_key, 1, ex60) # 4. 每天发送上限 daily_key fsms:daily:{scene}:{mobile}:{today()} if redis.incr(daily_key) 10: return error(DAILY_LIMIT_EXCEEDED) redis.expire(daily_key, 86400) # 5. 生成 6 位安全随机验证码 code generate_secure_code(6) # 6. 存储到 RedisTTL 5 分钟 cache_key fsms:code:{scene}:{mobile} redis.set(cache_key, json.dumps({code: code, attempts: 0}), ex300) # 7. 调用第三方短信服务商发送 sms_result send_sms(mobile, template_code, {code: code, minutes: 5}) # 8. 记录完整日志 logger.info(fsms_send mobile{mobile} scene{scene} fthird_party_success{sms_result.success} fthird_party_msg{sms_result.msg}) return success()这里有一个容易犯的错误频率限制的 Redis 写入操作不是原子的可能出现两个并发请求同时通过exists判断然后都去发送短信。正确的做法是用SET key 1 EX 60 NX代替existsset。NX表示只有 key 不存在时才能写入成功这样并发请求只有一个能拿到“锁”另外的自然而然落进“发送太频繁”的分支。校验验证码的逻辑def verify_code(mobile, scene, input_code): cache_key fsms:code:{scene}:{mobile} raw redis.get(cache_key) if not raw: return error(CODE_EXPIRED) data json.loads(raw) # 尝试次数控制最多 5 次 if data[attempts] 5: redis.delete(cache_key) return error(CODE_LOCKED) if data[code] ! input_code: data[attempts] 1 redis.set(cache_key, json.dumps(data), keep_ttlTrue) return error(CODE_MISMATCH) # 校验成功删除 key避免重复使用 redis.getdel(cache_key) return success()注意最后一步用了GETDEL而不是先GET再DELETE因为先取后删不是原子的极端并发下可能同一个验证码被两个请求同时校验成功。用GETDEL一次搞定就没有这个风险。如果你的 Redis 版本比较老不支持GETDEL可以用 Lua 脚本实现同样效果。还有一个常被忽略的点校验接口要不要做频率限制要。校验失败连续超过 5 次就删除验证码强制用户重新获取。否则攻击者可以拿着一个验证码在有效期内无限试错6 位纯数字的 100 万种组合在高速轰炸下是扛不住的。3.2 对接第三方服务商的具体步骤这一节讲实际对接流程。虽然每家服务商的 API 有差异但步骤大同小异无非是“申请账号、准备签名、创建模板、写代码、回调处理”这五步。第一步申请账号并开通短信服务。在服务商控制台创建 AccessKey这个密钥相当于你的 API 身份证权限能开多小就开多小只给短信服务相关权限。生产环境的 AccessKey 一定要放入配置中心或环境变量绝不能硬编码进代码仓库。我见过有人把 AccessKey 提交到 Git 仓库结果被爬虫扫到短信额度被拿去给各种人发了垃圾短信。第二步申请短信签名。签名是用户收到短信时显示在方括号里的名称比如“【某某系统】”。个人开发者申请签名会比较难通常需要企业资质、应用名称和相关证明。签名审核通过后发送短信时签名会直接生效不需要额外传参。第三步创建短信模板。模板内容类似“您的验证码为 ${code}${minutes} 分钟内有效请勿泄露。”创建后要等审核。审核重点是内容是否涉及营销、是否有明显诱导、是否有敏感词。验证码模板是最容易通过的模板一般几分钟到几小时就能审核完。模板通过后会有一个模板 ID发给template_code字段。第四步写发送调用。用服务商 SDK 就能完成。注意 SDK 的初始化方式通常需要传 AccessKeyId 和 AccessKeySecret。发送时传入手机号、模板 ID、模板参数。这里提醒一点手机号参数不要做任何加密、拼接操作直接传纯号码国际号码带上国家码比如大陆手机号就是 11 位数字。第五步处理状态报告回调。短信发送请求成功不等于用户收到。服务商会异步回调一个状态报告里面包含消息 ID、手机号、发送状态成功/失败/运营商拦截等。务必提供一个 HTTP 回调接口接收这些报告并把状态落到日志或数据库。有了状态报告用户说“我没收到短信”时你能直接查出来是“请求失败”还是“运营商拦截”还是“用户手机问题”。在对接过程中我强烈建议封装一个SmsProvider接口下面实现AliyunSmsProvider、TencentSmsProvider等类。业务代码只依赖接口不依赖具体实现。这样以后换服务商、加备用通道只改动实现类不用改业务代码。3.3 与业务流结合库存系统和 AI 客服场景举例光说接口设计太抽象我拿最近做的两个场景具体说说。先看库存系统。库存业务流里有一个典型场景库存管理员要把 50 台设备从 A 仓调拨到 B 仓。正常流程是管理员提交调拨单单据进入审批流仓库经理在后台点“通过”。但这里有一个隐患如果管理员的账号在异地被登录或者有人拿着管理员的已登录会话偷偷提交调拨单怎么办短信验证可以在提交环节加一道闸管理员提交调拨单时系统判断调拨数量是否超过阈值比如 20 台超过则强制要求输入短信验证码验证通过后单据才真正创建。这个设计的关键点在于验证码校验通过后要返回一个短时 token 给前端前端提交业务数据时带上这个 token后端校验 token 有效后才执行调拨逻辑。不能把“验证码校验通过”和“业务数据提交”拆成完全独立的两个接口否则攻击者可以单独调用业务接口绕过验证。更简单的做法是前端先调 verify 接口verify 通过后生成一次性 token 存 Redis提交调拨单时校验并销毁 token整个链路就闭合了。再看 AI 电商客服。用户在对话框里输入“我要退款”AI 机器人判断这是一个高风险意图先不直接调退款接口而是回复“为保障资金安全需要验证您的手机号请输入验证码”同时后端调用发送验证码接口。用户在对话框输入验证码AI 解析出数字校验通过后才去查订单信息。整个过程对用户来说是无感的聊天界面里多了一步验证而已。AI 客服场景有个特殊点发送验证码的触发条件不是用户点击按钮而是 AI 的自然语言理解模块判定意图。所以这里要非常小心误判。用户可能只是问问“退款流程是什么”AI 如果也触发验证码下发体验就很差。解决方案是把“身份验证”和“信息查询”拆成两个意图。只有 AI 判定用户想“执行”退款、改地址等操作时才触发验证码。另外AI 客服并发高验证码发送接口要支持较高的 QPS缓存和第三方调用都不能做成阻塞式的。发短信是个外部 IO一定要用异步方式避免拖慢对话响应。4. 常见问题与排查技巧实录4.1 收不到短信是接口问题还是通道问题用户反馈“一直收不到验证码”这是最高频的问题。排查时我一般按照下面的顺序来。先查服务端日志看发送短信的调用返回。如果返回“成功”说明请求已经到达服务商问题大概率出在通道或用户侧。如果返回“失败”通常是参数有问题比如手机号格式不对、模板 ID 不存在、签名未审核、AccessKey 权限不足。服务商一般会返回错误码把错误码直接粘进服务商的文档里查就行。如果服务商返回成功继续查状态报告回调。状态报告会告诉你最终结果。常见的有“DELIVERED”已送达、“UNDELIVERY”未送达、“BLACKLIST”被列入黑名单。如果是“UNDELIVERY”可能是用户手机信号差、欠费停机、或者短信被运营商拦截。如果是“BLACKLIST”说明该号码曾经被投诉过或者命中运营商拦截策略就比较麻烦了。还要考虑一种情况短信到了但被手机系统当成垃圾短信或放进了通知栏之外。很多安卓手机自带骚扰拦截验证码文本里如果带有“代办”“点击链接”等敏感词会被误判。所以短信模板措辞尽量简单带上品牌名和验证码即可别写多余营销内容。最后测试时要覆盖三大运营商。同一个短信模板在不同运营商通道上的通过率可能差异很大。我每次上线前都会用一个专用测试手机号列表分别用移动、联通、电信的号码试发一遍确认都能正常收到。4.2 验证码校验失败和过期问题校验失败先看提示是“验证码错误”还是“验证码过期”。如果是“错误”可能是用户看错了、多打了一个空格也可能真的是前端把验证码多传了一位。如果用户连续输错系统应该引导用户重新获取。如果提示“过期”但用户明明刚收到短信就要好好检查 TTL 是否设置得太短。我前面提过短信到达有延迟建议 C 端场景至少 5 分钟。另外还有时区问题如果 Redis 里的时间戳和服务端time()用的是不同时区可能导致过期判断提前或延后。实际开发中我统一用 Unix 时间戳存值和比较避免时区混乱。还有一个很隐蔽的坑同一个手机号在多个场景下各自获取验证码用户复制了登录场景的验证码到注册场景去填必然失败。这种情况提示“验证码错误”但对用户来说很困惑。优化做法是校验接口同时支持“用户输入验证码不区分场景”也就是把同手机号在不同场景下的验证码都拿来比对。但这样会把安全隔离打通我一般不推荐。更好的办法是前端在切换场景时清掉验证码输入框并提示用户重新获取。4.3 接口超时与重试机制第三方短信接口偶尔会超时这是不可避免的。关键是 app 端同步等待还是异步处理。如果你的业务接口是同步调用第三方最坏情况下用户要等 3 到 5 秒才看到结果体验很差。我的做法是发送验证码接口先做本地校验手机号格式、频率限制通过后把“发送任务”丢进异步队列立刻返回“发送中”给前端。前端轮询或等服务端推送再提示用户“验证码已发送请注意查收”。异步化之后要处理重试。第三方接口超时后不能无脑重发因为短信内容可能已经通过另一条路径发出去了重试会造成重复短信。我的规则是没有收到服务商明确的“请求失败”响应时不重试收到明确的失败响应比如空号、参数错误也不重试只有超时且状态不确定时才在 30 秒后重试一次。重试也要有次数上限超过 2 次直接标记失败并告警。异步任务的重试队列建议用 Redis 的延迟队列或者消息中间件来做。如果是简单的系统用 Redis List 定时任务扫描也能实现但要注意消息不能丢。生产环境我倾向直接用消息中间件比如 RocketMQ 或 RabbitMQ天然支持延迟消息和重试。4.4 安全与合规注意事项最后聊安全。短信验证接口是业务系统的入口之一很容易被薅羊毛。除了前面说的频率限制和人机验证还有几件事必须做。日志必须完整。每一次发送请求要记录手机号、场景、IP、设备标识、服务商返回码、状态报告回执。不要记录验证码本身但可以记录验证码的哈希值方便排查问题。日志保留时间按公司要求来至少保留 90 天应该不过分。验证码的传输链路要加密。客户端和服务端之间走 HTTPS防止中间人截获。Redis 中的验证码要加解密吗我个人觉得 Redis 本身在内网不直接对用户暴露比不加密风险低很多。但如果你们安全审计要求高可以对验证码做一次 AES 加密再存。加密会带来额外复杂度需要权衡。还要注意验证码在短信内容里的展示。短信中不要出现“验证码可用于修改密码”之类的话容易被骗子利用。标准话术是“验证码用于登录验证请勿转发给他人”。虽然这属于运营层面的文案但后端同学在创建模板时就要把关。敏感操作场景建议做二次校验除了验证码还可以要求用户输入身份证后 4 位或者进行人脸识别。这属于业务风控层面的叠加不在本文范围内但设计接口时预留extra_params字段后续扩展会容易很多。5. 写在最后的几点经验按我自己的经验短信验证接口本身不复杂真正决定成败的都是细节。频率限制要用原子操作校验成功要立即作废验证码第三方状态报告要接回来日志要留全。这些点每个单独拿出来都不难但漏掉任何一个后续都要花几倍的时间去补。如果你正在做库存系统、AI 客服或者其他业务系统的短信验证功能我的建议是先把场景画出来明确哪些操作需要验证、验证通过后怎么防绕过然后再去写代码。代码只是实现流程设计才是核心。开发完成后一定要做一次“故障演练”把第三方短信服务商的服务模拟成不可用看你的备用通道能不能顶上看前端会不会卡死看日志能不能快速定位。这个演练我每次上线前都做虽然没有一次真的用上但心里踏实。这期的内容就到这里。如果你也在做类似的功能欢迎按这个思路去落地遇到具体问题可以顺着文章里的排查表一步步查大部分坑都能自己绕过去。
返回列表