☰
106短信验证码接口对接全解析:从签名算法到双通道降级
2026/10/14 14:38:42 网站建设 项目流程

今年给一个内部工具系统接验证码短信,我把原来的共享通道换成了106专属通道,跑完整个对接流程才发现,网上大多数教程只讲了“调一个发送接口”,真正让人头疼的是签名算法、模板变量、回执状态、限频策略这一整套链路。这篇把整个106短信验证码接口对接的完整过程写出来,从申请通道、拆接口文档、写可用的发送代码,到上线后处理收不到短信、防轰炸、双通道降级,一次说清。适合刚接触短信API的后端同学,也适合正在挑短信服务商、准备从共享通道切换到专属通道的团队参考。

1. 为什么验证码一定要走106专属通道,而不是随便找个短信服务

我先说结论:如果产品里的短信验证码一个月只有几百条,用共享通道没太大问题;可一旦开始走量,或者对到达率和速度有要求,就必须换106专属通道。这里说的“专属”不是运营商单独给你拉一条物理线路,而是短信服务商在106号段下帮你分配了一条独立的发送通道,有单独的并发资源、优先级和通道质量保障。

1.1 共享通道为什么不太行

共享通道的特点是便宜、门槛低,但缺点是发送优先级排在后面,晚高峰时段特别容易积压。用户点“获取验证码”之后,短信可能要等十几秒甚至更久才到,超过验证码有效期,用户就会反复点击,产生更多短信费用,最终体验很差。

我遇到过一个实际场景:某次活动推广期间,共享通道在整点时段积压了几万条通知短信,验证码跟着一起排队,用户投诉率直接翻倍。换成专属通道之后,验证码和营销短信走了不同队列,验证码的发送优先级被明显提高,基本两三秒内就能收到。

1.2 理解短信发送的完整链路

你发的验证码,从你的服务器到用户手机,中间经过这几段:

你的服务器把发送请求提交给短信服务商的API服务,服务商把内容投递给运营商交换中心,运营商通过短信中心下发到用户手机终端,用户手机上显示的发送号码就是106开头的一长串。106号码段由运营商统一管理,企业需要实名报备后才能使用,这就是为什么我们看到的大多数正规验证码都是1069开头的号码。

这个链路里,服务商网关的质量决定了到达率和速度。垃圾短信过滤系统会在运营商侧拦截常见的关键词和异常发送行为,如果你的模板内容不规范,或者发送频率异常,短信会被拦在运营商侧,服务商自己往往也查不出原因。所以对接106专属通道,不只是改API地址,还要匹配一套规范的模板审核和发送策略。

1.3 挑选短信服务商时先看这几个硬指标

对接之前,先把服务商定下来。市面上的短信服务商很多,我没有指定一定要用哪家,但建议按这几个维度筛一遍:

  • 通道类型:确认是106码号的专属通道,不是共享通道,最好有通道编号可查。
  • 到达率:让服务商提供最近一个月的验证码类短信到达率数据,低于95%的要谨慎。
  • 并发能力:问清楚每秒钟允许提交多少条的QPS上限,以及超过之后是排队还是报错。
  • API稳定性:看服务商的接口可用性承诺,正常应该不低于99%。
  • 审核效率:模板审核时间从几分钟到几小时不等,越快越灵活。

选服务商的时候要让对方提供测试账号,先用测试环境把发送、回执、模板审核流程完整走一遍,确认没明显技术硬伤再谈价格。验证码类短信价格差异不大,真正重要的是通道质量和服务。

2. 对接前的凭据准备与接口文档拆解

拿到服务商的账号之后,第一步不是写代码,而是把接口文档里的鉴权方式弄明白。我见过太多人在签名算法上翻车,比如参数拼接顺序不对、大小写不统一、MD5没有转大写,请求直接被网关拒绝返回签名错误。

2.1 需要向服务商拿到哪些字段

一般服务商会给你以下信息,我列个表方便对照检查:

参数含义说明
appId 或 accessKeyId应用唯一标识服务商控制台创建应用后生成
appSecret 或 accessKeySecret私钥用于生成签名,绝对不能泄露到前端
signName短信签名比如“某科技有限公司”,需要在控制台报备
templateId模板编号模板审核通过后才可调用
平台域名API地址测试环境和生产环境是两套域名,容易搞混
回调接收地址状态回执URL接收短信到达状态通知

拿到这些信息后,把appSecret存到后端环境变量里,不要写死在代码仓库里,更不要出现在前端请求里。验证码发送接口必须由服务器端调用,如果前端也能调用,等于把发送能力暴露给了外部,很快就会被刷短信,产生巨额费用。

2.2 两种常见鉴权方式:请求签名与动态Token

服务商的鉴权方式通常有两种。第一种是每次请求都给参数做签名,服务商收到请求后按同样算法算一次签名,对比一致才放行。第二种是先用appId和appSecret调用一个认证接口换取动态token,再拿token去调发送接口,token一般有有效期。

我个人更喜欢签名方式,因为请求无状态,不用维护token的缓存和刷新,只要算法一致就能用。动态token方式需要处理token过期、并发刷新等逻辑,如果服务商文档没有把有效期写清楚,调用时很容易出现“偶尔成功偶尔失败”的现象。无论用哪种方式,都要把服务商示例代码里的算法细节抠出来,确保和文档描述一致。

2.3 签名计算最容易翻车的几个细节

签名算法的常见套路是:把除签名外的请求参数按参数名ASCII码字典序排序,再按照“参数名=参数值”的格式拼接成字符串,各参数之间用&连接,最后在末尾拼接appSecret,做MD5后转大写作为sign值。

我踩过的一个坑是:服务商要求参数值先做URL解码再去签名,我却拿编码后的值直接算签名,结果每一条请求都返回签名校验失败。排查了很久才发现,是因为我把手机号里的+号编码成了%2B去参与签名,而正确做法是用原始值参与签名。

另一个细节是参数大小写。有些服务商要求参数名保持小写,有些要求首字母大写,这个必须严格照着文档来。还有的服务商要求最终签名转大写,有的不转,最好在文档里明确标注。总之,签名计算的唯一标准是服务商服务端如何校验,不是你自己觉得合理就行。

3. 核心发送接口的调用细节与响应处理

鉴权逻辑理清之后,发送接口本身就很直接了。这里我给出一个用Python写的发送验证码的最小示例,适配常见的POST表单加签名方式,生产代码里还要加上日志、超时重试、异常捕获等逻辑。

3.1 一个最小可用的发送请求长什么样

import hashlib import time import json import requests APP_ID = "your_app_id" APP_SECRET = "your_app_secret" SIGN_NAME = "你的签名" PHONE = "13800000000" TEMPLATE_ID = "123456" CODE = "482913" def md5_sign(params, secret): # 按字典序排序参数名 sorted_keys = sorted(params.keys()) raw = "&".join([f"{k}={params[k]}" for k in sorted_keys]) raw += "&key=" + secret return hashlib.md5(raw.encode("utf-8")).hexdigest().upper() params = { "appId": APP_ID, "phone": PHONE, "signName": SIGN_NAME, "templateId": TEMPLATE_ID, "vars": json.dumps({"code": CODE}, ensure_ascii=False), "timestamp": str(int(time.time() * 1000)), } params["sign"] = md5_sign(params, APP_SECRET) resp = requests.post("https://api.example.com/sms/send", data=params, timeout=10) print(resp.json())

需要特别留意的两个参数。第一个是vars,它通常是一个JSON字符串,服务商要求用你模板里定义的变量名,比如模板内容是“验证码#code#,#time#分钟内有效”,那vars就要传{"code":"482913","time":"5"},变量名和多一个少一个都会导致模板匹配失败。第二个是timestamp,一般取毫秒级时间戳,有的服务商限制五分钟内的请求才有效,时间误差太大会返回请求超时。

3.2 响应码的分类处理框架

接口返回的HTTP状态码通常是200,具体业务结果要看JSON里的code字段。从经验来看,验证码接口的返回码大概就几类,建议你做成一个统一处理框架,而不是每个调用点各自判断:

返回码含义处理动作
0或00000提交成功更新发送记录,等待状态回执
签名错误/鉴权失败appId、签名算法或密钥不对打印原始参数和签名串排查
IP白名单限制当前出口IP未登记到控制台添加服务器公网IP
余额不足账户余额不够自动告警,防止静默失败
模板/签名未审核模板还在审核中或已驳回先查审核状态,不要改代码
手机号格式错误号码异常过滤后再发起请求
触发限频同号码或同IP请求过于频繁返回“请稍后再试”,进入频控流程
内容含敏感词模板内容被风控拦截修改模板重新报备

我建议把这个映射做成一张表放在服务端配置里,每个返回码对应一个用户可读的提示文案。这里有个容易被忽略的点:发送接口返回成功,只代表服务商已受理,不代表用户已收到短信,真正的结果是靠状态回执拿到的。所以发送记录表里至少要有一个state字段,先标记为“发送中”,收到回执后再更新为“成功”或“失败”。

3.3 模板变量的传参格式与长短信计费陷阱

验证码模板的变量传参看似简单,实际上有几个隐藏坑。第一个是中文字符编码,部分服务商的旧接口对vars里的中文不友好,要先用URL编码再传,或者用JSON整体提交。我一般优先选支持application/json请求体的接口,省去编码问题。

第二个是超长短信问题。短信按字计费,常见规则是70个字符以内算一条,超过70个字符会被拆成多条发送,费用翻倍。验证码场景里模板文案加变量一般不会超长,但有些团队喜欢在文案里加很多提示内容,比如“本验证码有效期5分钟,逾期请重新获取,如有疑问请联系客服,请勿泄露给他人”,一旦超过70字符,计费就从一条变成两条。想控制成本,模板就尽量精简。

对账的时候还有个细节:服务商那边是按实际发送成功的短信条数扣费,还是按提交条数扣费,这两种口径差别很大,尤其是触发限频后反复提交时。确认计费口径之后,再对整个发送量做预算管控,避免月底账单吓一跳。

4. 状态回执与异步通知:别等发送接口返回就以为万事大吉

验证码系统里最容易出问题的不是发送接口,而是状态回执。我见过一个团队上线第一周短信费用高了两倍,排查后才发现他们把“服务商受理成功”当成了“用户收到”,于是每次都不等回执就允许用户重发,短时间重复下单,费用直接翻倍。

4.1 发送成功和到达成功是两回事

服务商的发送接口返回成功,只表示短信已经进入服务商队列;短信能不能到达用户手机,取决于运营商侧是否拦截、号码是否停机、手机信号是否正常一系列因素。所以正规服务商都会提供一个状态回执回调,异步告诉你每一条短信最终的状态。

常见的回执状态包括成功投递、用户关机或不在服务区导致失败、超时未投递、运营商拦截等。不同服务商的状态码命名有差异,但大部分都遵循“DELIVRD代表成功投递”这样的约定。对接时要把状态码做成映射表,未知状态码统一记日志,不要默认当成成功。

回执消息体里一般会带上你提交时的msgId、手机号、发送状态和时间戳。你可以拿msgId和数据库里的发送记录关联,更新对应那条短信的状态。这个msgId最好在调用发送接口时就持久化下来,它是整条短信全生命周期里唯一的追踪标识。

4.2 回调地址的幂等设计与重试机制

状态回执是服务商主动POST到你服务器的接口上,这个接口有几个硬性要求。

第一是必须幂等。服务商可能因为网络超时对同一条回执推送多次,你的处理逻辑不能因为收到两次回执就把状态更新成两个值。我的做法是拿msgId做唯一索引,第一次更新状态,后续重复推送直接忽略。

第二是必须有快速响应。服务商不知道你的逻辑复杂度,只要求你在几秒内返回固定字符串,比如SUCCESS或OK,表示你已收到。如果你的处理函数里插入太多耗时的操作,比如同步请求第三方接口,很容易超时导致服务商重推。

第三是要考虑回调丢失场景。网线抖动、服务重启、代码异常都可能导致某一条回执没收到。我的兜底方案是每天跑一个对账任务,把超过两小时仍是“发送中”的记录捞出来,再调用服务商提供的状态查询接口主动核实一次。成本不高,但能把丢失的回执补回来。

4.3 我对回执消息体的一段真实踩坑

有一回线上验证码到达率从99%降到92%,用户反馈收不到短信,我打开日志发现很多msgId一直停在“发送中”,没有回执消息进来。后来查了一遍回调地址的日志,才发现对方推送的回执内容不是JSON而是application/x-www-form-urlencoded格式,我们却按JSON去解析,全部解析失败。虽然返回了SUCCESS,但服务商侧认为消息已经消费成功,不再重推,状态就这样丢了。

那次之后我长了个教训:不管服务商文档里写的是JSON还是form格式,都要先用真实回执报文存一份原始记录,再做解析。这样就算格式变了或者解析失败,原始报文还在,可以补推或手工处理,不至于彻底丢事件。

5. 上线后必踩的坑:限频、拦截与调试技巧

不管发送接口对接得多顺,上线之后总会有几类问题缠着你。这里说几个我真实遇到过、而且网上的文档基本不会告诉你的场景。

5.1 手机收不到短信的排查顺序

用户说“没收到验证码”,先别急着发第二次,按照下面这个顺序排查:

  1. 查发送记录里接口返回码是不是提交成功,如果没有提交记录,去看前端有没有真的触发请求。
  2. 查状态回执,如果回执显示成功投递,问题大概率在用户手机端;如果回执显示失败,按失败原因分类处理。
  3. 如果既没有回执也查不到消息,调用服务商的状态查询接口主动核验。
  4. 用测试手机号实际提交一条,对比自己和用户的手机号是不是同一运营商、同一号段,有些号段对短信发送更严格。

一个常见迷惑情况是回执显示成功但用户就是没收到,尤其是双卡手机。很多用户主卡收到短信会弹通知,但双卡手机里如果两条卡都装了短信应用,验证码可能落在另一张卡上。这种情况不是链路问题,在线客服引导用户翻一下短信拦截、双卡收件箱就好。

5.2 防轰炸限频怎么做

没有频控的验证码接口,几分钟内就能被外部脚本刷掉几万条短信。频控策略至少要覆盖两层:

  • 同一手机号维度:60秒内只能发1条,24小时内最多不超过10条,超过就提示“获取太频繁”。
  • 同一IP维度:同一IP每分钟最多发5条验证码,超过直接拒绝。

如果业务允许,我会在获取验证码之前先加一道人机校验,比如滑块或者点选验证码。加这一层能挡住大部分脚本,短信费用会明显下降。这个在设计阶段就要规划好,等活动流量上来再补频控,很可能已经损失一笔短信费了。

5.3 测试环境没有真机怎么办

测试环境下开发机的手机号反复提交验证码,很快会被服务商限频,长此以往测试号会被拉入黑名单。我的办法是申请一个专用的测试模板,模板文案固定为固定数字,走服务商的测试专用通道,不占用真实通道和真实计费。如果没有测试通道,就把验证码发送结果模拟化:在配置中心加一个开关,打开后发送接口不真正调用服务商,而是直接落库并在日志里输出验证码,前端照样能走完整登录流程。

模拟发送不是异想天开的后门,而是为了把外部依赖从开发环境里解耦掉,让前后端联调不再依赖短信服务商的可用性。唯一的风险是生产环境一定要确认开关处于关闭状态,最好在上线前做一次配置核查。

5.4 高峰期并发与降级预案

验证码请求往往集中在某个时刻,比如活动开抢前几分钟,用户疯狂点按钮,瞬时QPS能是平常的几十倍。服务商的专属通道有并发上限,超过之后是排队还是拒绝,取决于服务商的具体实现。高峰期还没到的阶段,就要提前做好两件事:一是了解服务商并发上限,设置本地线程池和队列,避免一个调用方的慢请求拖垮整个服务;二是准备降级策略,主通道超时后切换到备用通道或者备用服务商,保证验证码链路可用性。

降级方案要写成配置,不要临时写代码。我的习惯是维护一份短信通道配置表,包含主通道、备用通道、超时时间、熔断阈值,配置变更后立即生效,不需要重新发布。这个表可以做成简单的配置中心,也可以落在数据库里加一层缓存,看团队基础设施。

6. 从能用到好用:验证码链路还要关注什么

发送接口通了、回执处理好了、频控加上了,这个项目才算真正能做上线评审了。但在这个基础上,我还有几个长期收益更高的优化方向会顺手做掉。

6.1 验证码有效期与防暴力破解

验证码本身是一个重要的安全边界。有效期太短影响用户体验,太长又容易被撞库或爆破。一般建议5分钟,业务安全等级高的可以缩短到2分钟。服务端每次校验验证码后必须立即删除,不能让它进入下一次校验,否则同一个验证码被重放也会产生风险。

更稳妥的方案是验证码和会话绑定:生成时将手机号和操作场景一起存进缓存,校验时同时比对手机号、场景和验证码。我用一套这样的规则之后,再也没遇到过验证码跨场景使用的漏洞。不要只在代码里比较用户提交的验证码和缓存中的验证码,这种简单做法会漏掉很多边界情况。

6.2 模板审核与文案规范的一些注意事项

验证码模板的审核重点在于文案里不能有诱导行为和恶意链接,也不能包含类似“免费”“中奖”这种营销敏感词。初次提交模板建议只保留基本要素:验证码、有效时间、非本人操作提示。线上运营如果想在验证码里加一句活动引导,最好单独提交活动模板,和纯验证码模板分开。

变量位的定义也要规范。服务商一般要求在模板中用#号占位,比如#code#。禁止把签名、号码或者营销词放进变量位置,一旦被发现会被标记为异常模板甚至关停通道。作为对接方,这些规则要提前同步给产品同学,省得产品设计了一个“验证码#code#,欢迎领取#gift#”,最后模板审不下来又来回改。

6.3 双通道容灾和成本控制

我建议在1个专属通道之外,至少再备一个不同服务商的通道,平时可以不用,但配置要在那边准备好,模板要提前审核通过。主通道出现大面积故障的时候,直接把流量切到备用通道,不切换的话我们只能被动等待服务商恢复,用户侧就是持续收不到验证码。切换动作要可以在1分钟内完成,甚至做成自动熔断。

成本控制这块,验证码短信单价已经很低,真正的浪费基本来自重复发送和无效发送。我做过一次统计,当时系统里30%左右的验证码都发给了未注册用户,或者用户输错手机号导致的无效短信。后来在发送前加了手机号格式校验和注册身份检查,费用降了接近一个数量级。与其纠结几分钱一条的单价,不如先从发送策略上把无效量降下来。

最后再分享一个我自己的习惯:每次调整短信相关的代码或配置,我都会完整跑一次全链路验证,从发送接口提交、库里的发送记录更新、回执回调接收、验证码校验成功,到失败场景的频控拦截和模板报错,全部录在测试文档里。短信链路涉及外部依赖,很多问题在本地测不出来,这套回归流程能让我在每次发布前多一份把握。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询