ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从零搭建短信验证码接收平台:接收、存储、查询与回调全链路教程

从零搭建短信验证码接收平台:接收、存储、查询与回调全链路教程 做应用开发这些年短信验证码这东西看着不起眼但真到联调阶段能把人逼疯。自己手机号不敢乱填、测试环境收不到短信、第三方平台回调迟迟不来、验证码过期了才发现没存下来——这些问题几乎每个后端和测试都遇到过。所以我花了两个周末从零搭了一套短信验证码接收平台把接收、存储、查询、通知、管理后台全链路打通顺手整理成这篇超详细教程。无论你是做API开发、自动化测试、第三方登录对接还是想给业务系统加个验证码兜底通道这篇都能直接抄作业。整个平台本质上干的事情很简单把散落在各处的短信验证码统一收拢到一个地方再通过HTTP接口或管理后台随时查。但真正落地的时候接入方式选型、数据模型设计、过期策略、回调防重、安全签名这些细节才是大头一个没想清楚后面就要返工。下面按我的实际搭建顺序从架构设计讲到部署上线把每一步的理由和坑都说明白。1. 平台定位与整体方案设计1.1 这个平台到底解决什么问题短信验证码接收平台不是一个给普通用户用的产品它解决的是开发和测试环节里的一个具体痛点你没法稳定、可控地收到验证码。比如你在做用户注册流程的自动化测试每次跑用例都要真实填一个手机号、等短信、再读验证码填回去短信延迟一波动整个用例就挂了。再比如你对接了某个第三方服务对方要求你提供接收回调的地址来收验证码通知你总不能在本地起个服务等它回调。所以这个平台的价值就三条第一把验证码接收入口统一成标准HTTP接口方便任意语言调用第二把验证码和手机号、业务标识、时间关联起来能按条件快速检索第三提供管理后台人也能直接查不用翻日志。想明白这三点后面的架构就不会跑偏。1.2 两条接入路线回调转发还是硬件模块接收短信验证码本质上就两条路要么从云短信服务商那里拿回调要么自己搞一个能收短信的硬件网关。两条路我都试过感受完全不同。云短信服务商回调是首选也是绝大多数业务场景适用的。原理是你把验证码发到某个手机号之后运营商或短信平台会把短信内容以HTTP回调的形式推送到你指定的URL你的平台负责接收、解析、入库。这种方式零硬件成本、接收延迟低、部署简单而且只要能收到回调验证码内容、发送时间、手机号这些字段都是结构化带过来的解析非常干净。硬件GSM模块方案则是兜底方案适合没有云短信服务商资源、或者要收的是真实手机卡短信的场景。常见做法是搞一个树莓派或者工控机插上SIM800L之类的GSM模块模块收到短信后通过串口把内容读出来再POST到你平台的接口。这个方案的好处是短信真正落在你自己的卡上缺点也明显硬件要维护、SIM卡要养号、信号不稳定、并发一高就丢消息。我建议按需选择不要一上来就上硬件先把回调方案跑通再说。1.3 技术选型为什么用 FastAPI Redis SQLite技术栈我选的是Python FastAPI Redis SQLite没上重型中间件。原因很实际这个平台的定位是轻量级内部工具不是高并发业务系统。FastAPI写起来快、自带API文档、异步性能也够用Redis用来做验证码的短期缓存和过期控制天然支持TTL省去自己写定时任务删数据的麻烦SQLite作为持久化存储单文件备份方便数据量到百万级之前完全扛得住。你要是更习惯Java或Go这套架构思路完全能平移代码逻辑都是一样的。核心区别只在语法层。我倾向把重心放在设计思路上不要纠结用哪个框架先跑通再优化。2. 核心数据模型与验证码生命周期2.1 数据表设计与字段含义先看表结构这是整个平台的骨架。我设计了一张sms_code表字段如下字段名类型说明idINTEGER 主键自增唯一IDphoneVARCHAR(20)接收验证码的手机号codeVARCHAR(10)验证码内容messageTEXT完整短信原文biz_idVARCHAR(64)业务标识比如注册、登录、找回密码channelVARCHAR(32)来源渠道aliyun/twilio/gsm等statusTINYINT0待使用 1已使用 2已过期expire_atDATETIME过期时间created_atDATETIME接收时间notifiedTINYINT是否已推送给业务方0/1每个字段都有用途重点说几个容易忽略的。biz_id很关键业务方调用查询接口时往往不是只查一个手机号下的所有验证码而是带着自己的业务单号来查没有这个字段查询就抓瞎。message必须存完整原文因为验证码解析可能出错留原文方便事后核对。notified这个字段很多人会省但它是做回调通知防重的基础没有它回调推送失败重试时就无法判断是否已经推过。2.2 验证码状态机待使用、已使用、已过期验证码的生命周期很清晰三种状态来回流转待使用消息接收成功、解析出验证码、入库完成后的初始状态。已使用业务方通过接口标记该验证码已被消费防止同一验证码被重复使用。已过期到达预设有效期后自动进入查询接口默认不返回过期验证码。状态流转要在代码里强制约束不能只在界面上显示。我实现的逻辑是查询接口返回待使用状态的验证码标记使用时通过UPDATE ... WHERE status 0的方式做乐观锁如果更新影响行数为0说明已经被消费或过期直接返回失败。这样即使在多线程并发场景下同一个验证码也只会被消费一次。2.3 过期策略与防重复消费过期策略我用了双保险。Redis里存一份keyTTL设为验证码有效期默认5分钟到点自动消失SQLite里也维护expire_at字段查询时用SQL过滤expire_at now()。这样即使Redis数据被清掉数据库层依然有兜底。这里有个细节值得说验证码的有效期应该从短信发送时间算而不是从平台接收时间算。云短信服务商回调里通常会带发送时间没有的话就取平台接收时间近似。5分钟有效期是个经验值业务上一般短信验证码都是5分钟有效你按自己业务调整即可。过期时间到了不要物理删除数据保留记录便于审计只靠status2标记过期就行等哪天数据膨胀了再定期清理。3. 短信接入层实现从收到短信到入库3.1 方案A云短信服务商回调Webhook接入云短信回调的接入逻辑是最顺的。以阿里云短信为例你在控制台配置上行消息回调地址指向平台的/webhook/sms接口即可。服务商收到短信后会把消息内容POST到这个地址你的接口只需要验签、解析、入库三步。下面是我实现的FastAPI回调接口核心代码# webhook.py import hashlib import hmac import json from datetime import datetime, timedelta from fastapi import APIRouter, Request, HTTPException router APIRouter(prefix/webhook, tags[webhook]) def verify_signature(payload: bytes, signature: str, secret: str) - bool: 服务商会用 secret 对请求体做 HMAC-SHA256 签名 我们这边用同样的方式计算一次对比是否一致。 expected hmac.new( secret.encode(utf-8), payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature) router.post(/sms) async def receive_sms_callback(request: Request): 接收云短信服务商的回调。 兼容阿里云/Twilio等常见格式字段名不同就做个映射。 body await request.body() headers request.headers signature headers.get(X-Signature, ) secret your_webhook_secret # 与短信平台配置一致 if not verify_signature(body, signature, secret): raise HTTPException(status_code403, detailsignature verify failed) data json.loads(body) # 字段映射兼容不同服务商 phone data.get(phone) or data.get(mobile) or data.get(To) code data.get(code) or data.get(smsCode) or data.get(Body) message data.get(message) or data.get(text) or data.get(content) channel data.get(channel, unknown) if not phone or not code: raise HTTPException(status_code400, detailmissing required fields) # 入库 缓存 通知一并处理 await save_sms_code( phonephone, codecode, messagemessage, channelchannel, expire_atdatetime.now() timedelta(minutes5), ) return {code: 0, msg: ok}这段代码里最容易被坑的是验签。很多服务商不是简单放一个签名头而是把签名放在 query 参数里或者对 body 做base64之后再签名你按他们的文档调整verify_signature里的签名算法就行。我建议先不开启验签跑通全流程确认回调能正常收到之后再打开验签排查起来更快。3.2 方案BGSM模块自建短信网关如果你没有云短信服务商资源而是要用真实SIM卡收短信那就用GSM模块方案。硬件连接方式很简单SIM800L模块的VCC接5V电源、GND接地、TXD/RXD分别接树莓派的RX/TX插上SIM卡、接好天线然后通过Python的pyserial读取串口数据。核心逻辑是监听串口当有新短信进来时AT指令会主动推送CMTI: SM, index这样的通知然后你发送ATCMGRindex读取指定索引的短信内容解析出手机号和验证码再POST到平台的接收接口。这里有个大坑SIM卡短信存满之后新短信会收不到所以每读完一条就要用ATCMGDindex删除保持存储空间可用。GSM模块收到短信内容后如果验证码和手机号是混在一起的用正则提取就行。比如短信内容【XX网】您的验证码是1234565分钟内有效用re.search(r(\\d{4,6}), message)就能把验证码抠出来。整个过程不复杂但硬件调试耗时我前前后后折腾了一天才稳定信号不好时收发延迟会到十几秒要有心理准备。3.3 入库逻辑与消息队列解耦回调接口收到短信后不能直接同步写入数据库就算完。因为入库之后往往还要做两件事往Redis写缓存、向外推送通知给业务方。这三件事如果串行做回调接口的响应时间会被通知推送拖慢服务商那边超时就会重推造成重复数据。我的做法是入库之后把“推送通知”这个动作塞进一个内存队列由后台任务池异步消费回调接口只负责快速落库并返回。如果你用Celery或者其他任务队列也完全可以原理一样。FastAPI里最简单的做法是用BackgroundTasksfrom fastapi import BackgroundTasks async def save_sms_code(phone: str, code: str, message: str, channel: str, expire_at: datetime): # 入库 SQLite insert_sql INSERT INTO sms_code (phone, code, message, channel, status, expire_at, created_at) VALUES (?, ?, ?, ?, 0, ?, ?) await db.execute(insert_sql, ( phone, code, message, channel, expire_at.strftime(%Y-%m-%d %H:%M:%S), datetime.now().strftime(%Y-%m-%d %H:%M:%S), )) # 写 RedisTTL 5 分钟 redis_key fsms:latest:{phone} await redis.setex(redis_key, 300, json.dumps({ phone: phone, code: code, message: message, channel: channel, })) # 异步推送 background_tasks.add_task(notify_business, phone, code, message) router.post(/sms) async def receive_sms_callback(request: Request, background_tasks: BackgroundTasks): ... await save_sms_code(..., background_tasks) return {code: 0, msg: ok}这样做的好处是回调接口的响应时间稳定在毫秒级服务商不会因为超时重推。入库和通知解耦后哪怕通知服务挂了也不影响短信的正常接收和存储。4. 查询API与异步通知设计4.1 查询接口按手机号或业务标识拉取验证码查询接口是平台对外输出能力的主要入口。业务方的典型场景是测试脚本执行到填写验证码这一步时调用你的接口传入手机号拿到验证码再填回去完成流程。所以接口设计要简单直接参数越少越好。# query_api.py from fastapi import APIRouter, Query router APIRouter(prefix/api, tags[api]) router.get(/sms/code) async def get_sms_code( phone: str Query(..., description手机号), biz_id: str Query(None, description业务标识可选), ): 查询最新一条未使用的验证码。 默认返回当前时间前5分钟内、状态为待使用的记录。 sql SELECT id, phone, code, message, biz_id, channel, status, expire_at, created_at FROM sms_code WHERE phone ? AND status 0 AND expire_at ? ORDER BY id DESC LIMIT 1 row await db.fetch_one(sql, (phone, datetime.now().strftime(%Y-%m-%d %H:%M:%S))) if not row: return {code: 1, msg: no available sms code, data: None} if biz_id and row[biz_id] ! biz_id: return {code: 1, msg: biz_id mismatch, data: None} return {code: 0, msg: ok, data: dict(row)}查询逻辑有一个重要的取舍默认只返回最新一条待使用验证码而不是返回所有记录。原因是一般业务场景下你只需要最新的那个验证码返回太多反而容易取错。如果你确实需要全部记录加一个limit参数让调用方自己控制即可。4.2 回调通知验证码到达后主动推送给业务方查询接口是拉模式业务方主动来取。但有些场景更适合推模式——验证码一到达平台主动POST到业务方指定的回调地址业务方不用轮询。我在平台里加了一个notify_url配置业务方注册时会绑定自己的回调地址和密钥。推送逻辑用简单重试策略第一次推送失败后间隔5秒、30秒、2分钟各重试一次最多4次。同时依赖数据库里的notified字段判断状态推送成功置14次都失败则保持0方便事后人工补推。重试过程中要特别注意消息幂等业务方的接收接口应该按验证码ID去重否则重试会导致业务处理两次。4.3 接口签名与安全防护平台内部自己用的时候可以不做签名但一旦要暴露给多个业务方签名认证就必须加。我的实现是每个业务方分配一个app_id和app_secret调用查询接口时请求头带上X-App-Id和X-Timestamp同时用app_secret对请求参数做HMAC-SHA256得到一个X-Signature头。服务端用相同方式计算签名并对比同时检查时间戳超过5分钟视为过期请求。这个设计主要防两件事一是防止接口被未授权的第三方调用二是防止请求被截获后重放。时间戳校验就是防重放的因为验证码数据本身敏感接口暴露在公网被乱抓就麻烦了。除了签名我还在Nginx层加了IP白名单和限流双保险。5. 管理后台与可视化查询5.1 后台页面功能清单管理后台我用的是一个很朴素的思路一个HTML页面 一个查询接口不做复杂前端框架。页面功能就这么几个按手机号查询输入手机号展示该号码最近接收的验证码记录。按时间筛选默认显示最近30分钟可切换1小时、24小时。标记已使用每条记录旁边有个按钮手动把状态改成已使用。查看原文点击展开查看完整的短信原文和原始回调数据。后台不需要做登录注册一个简单的Token校验就够了部署在内网最省事。如果非要暴露公网建议在Nginx层加HTTP Basic Auth多一层保险。5.2 查询操作与交互细节后台的交互细节比很多人想象的重要。我踩过几个坑验证码列表默认按接收时间倒序排这个必须做到否则你看到的第一条不是最新的会被误判每条记录要显示接收时间的时区这个也必须有不然对不上时间线已使用的验证码要有明显的视觉区分我用了灰色标签避免误用。另一个小细节是查询结果里要把验证码单独列出来加粗显示不要混在短信原文里面。因为测试人员在实际操作时是要把验证码复制出去的他们不会去看原文。后台页面虽然简单但要围绕“快速找到验证码”这个核心目标来设计其他花里胡哨的功能都是干扰。6. 部署上线与稳定性优化6.1 Docker Compose 一键部署部署方式我推荐Docker Compose一条命令起全套省去环境搭建的麻烦。目录结构如下sms-platform/ ├── app/ │ ├── main.py │ ├── webhook.py │ ├── query_api.py │ ├── database.py │ ├── models.py │ └── requirements.txt ├── Dockerfile ├── docker-compose.yml └── data/ └── sms.dbdocker-compose.yml里只需要两个服务一个FastAPI应用一个Redis。SQLite数据库文件通过volume挂载到宿主机方便备份。内容如下# docker-compose.yml version: 3.8 services: redis: image: redis:7-alpine container_name: sms-redis restart: always ports: - 6379:6379 app: build: . container_name: sms-platform restart: always ports: - 8000:8000 environment: - REDIS_URLredis://redis:6379/0 - DATABASE_PATH/app/data/sms.db - TOKENyour_admin_token volumes: - ./data:/app/data depends_on: - redis启动命令就是docker compose up -d --build访问http://服务器IP:8000/docs就能看到FastAPI自动生成的接口文档管理后台页面放在http://服务器IP:8000/admin。6.2 关键配置项说明配置项的坑主要集中在环境变量上。我强烈建议把所有可变参数都抽到环境变量里不要在代码里硬编码。关键配置有这么几个环境变量默认值说明REDIS_URLredis://localhost:6379/0Redis连接串DATABASE_PATH./sms.dbSQLite数据库文件路径TOKENadmin123管理后台访问TokenWEBHOOK_SECRET空回调接口验签密钥CODE_TTL_SECONDS300验证码有效期秒数这些配置看起来简单但每一项都在生产环境里坑过我。比如DATABASE_PATH容器里如果不挂载volume重启一下数据库就没了数据全丢。再比如CODE_TTL_SECONDS刚开始写死5分钟后来业务方要求改成10分钟改代码重新部署太麻烦抽成环境变量一行就搞定。6.3 稳定性监控与告警平台稳定性监控一开始我觉得没有必要反正就是个内部工具。直到有一次云短信服务商回调突然断了一个多小时我完全没察觉业务方找过来才发现。从那以后我加了三层监控第一层心跳接口。加一个/healthz返回数据库和Redis的连接状态配合云监控平台做探活挂了自动告警。第二层数据量监控。每分钟统计接收的验证码数量写入日志如果一段时间内数量骤降为0大概率是回调链路出问题了。第三层推送失败告警。如果通知业务方的notified字段长时间保持0说明推送链路有异常需要人工介入。这三层监控不需要专门搭一个监控系统最简陋的做法就是写几个脚本定时跑发现问题往企业微信或钉钉群发个消息。监控的核心是“能发现问题”而不是“用什么工具发现”。7. 常见问题与排查技巧实录7.1 收不到验证码短信这是最常遇到的问题一上来怀疑云短信服务商有问题其实大半是本地配置问题。按照我排查的经验顺序应该是先看回调日志有没有请求进来没有请求就说明回调地址没配置对或者服务商那边根本没把短信发出去有请求但入库失败就看是不是验签没过、字段解析失败入库成功但查询不到就看是不是状态被提前改成已使用或者Redis和SQLite的过期时间不一致。这一套排查下来绝大多数收不到短信的问题都能定位。千万注意不要跳过看日志这一步去猜原因回调日志是你排查的第一手资料必须保留。7.2 回调超时或重复推送云短信服务商的回调是有超时时间的一般3到5秒。如果你的回调接口里做了耗时操作比如同步推送通知响应慢了服务商就会重推造成同一个验证码入库多条记录。这个问题根因就是入库逻辑里没做幂等。解决办法有两个层面第一入库前检查手机号加验证码加接收时间的唯一索引重复的直接忽略第二把耗时操作移出回调请求链路异步执行。两件事都做了重复推送的问题基本绝迹。另外还要注意即使做了幂等日志里也要保留每次回调的原始记录方便核对。7.3 验证码乱码或解析失败验证码解析失败通常出现在GSM模块方案里云回调方案很少见。GSM模块收到的短信内容会受编码影响常见的情况是中文短信内容变成乱码但验证码是数字所以还能提取出来。如果连数字都解析不了大概率是短信内容里验证码格式不是纯连续数字比如验证码为 1 2 3 4 5 6中间有空格正则匹配就失败。我的处理方案是用两步提取第一优先用正则匹配连续数字第二如果匹配不到就按常见关键词切分取关键词后面的字段再清除非数字字符。这个兜底逻辑不完美但应付常见格式足够了。另外GSM模块方案里的AT指令解析也有编码坑串口默认可能是7-bit或8-bit模式要按SIM卡的字符集调整否则中文必乱。7.4 时区与过期时间坑时区问题是那种“看起来没问题跑起来全是问题”的隐藏坑。FastAPI默认用的是服务器本地时间如果你服务器是UTC时区而你和业务方都在东八区那么你存的created_at和业务方看到的时间就对不上。我吃过这个亏排查了半天才发现是时区偏移。统一方案是服务器统一设置成Asia/Shanghai所有时间字段存储和展示都用东八区时间。在Docker环境里需要在启动命令里加-e TZAsia/Shanghai同时代码里也强制指定双保险。过期时间的计算也基于东八区时间不要混用UTC和本地时间否则验证码明明没过期却显示过期查起来特别抓狂。8. 合规边界与我的个人体会8.1 合规使用场景提醒这个平台本身是开发工具但它接收的是短信验证码这种敏感信息所以使用边界一定要讲清楚。合法的场景是你自己业务系统里的测试号码回调、你开发的自动化测试脚本、你自建的短信网关监控。一句话总结就是平台只能接收你自己能合法接收的短信不能用于绕过实名认证、批量注册、恶意薅羊毛等违法违规场景。另外如果平台里存储了真实的手机号和短信内容要注意隐私保护。至少要做到三点数据库文件权限收紧、管理后台访问加认证、日志脱敏处理。不要觉得内部工具就无所谓数据安全这根弦任何时候都不能松。8.2 个人实操体会这套平台前前后后改了三版第一版只是简单把回调存进数据库第二版加了查询接口第三版才补上Redis缓存、通知推送、签名认证。每一次改动都是被真实场景逼出来的不是一开始就设计好的。所以我特别建议你第一次搭的时候先跑通最小闭环不要贪大求全后面遇到具体需求再迭代加功能。如果要扩展这个平台可以考虑的方向有验证码识别接入OCR模型自动识别短信验证码图片多租户支持给不同业务方分配独立密钥和存储隔离监控数据可视化。但这些都属于锦上添花核心链路反而是多多打磨细节——验签、幂等、过期、防重、监控把这些做到位平台才能真正扛得住生产环境的折腾。
返回列表