做短信验证码登录这件事,我前前后后在好几个项目里都接过,从最简单的单机应用,到后面基于若依微服务框架的一整套环境迁移到阿里云ECS,中间踩过的坑可以说相当多。最惨的一次是上线当晚,用户怎么点都收不到验证码,排查到快崩溃,最后发现是RAM子账号权限漏了一条。这篇文章就把我整个使用阿里云短信的过程记录下来,从开通配置、签名模板申请、验证码登录的完整实现,再到接口测试、若依框架集成、以及迁移云环境和压测时的注意事项,全部摊开来讲,希望能帮你少走点弯路。
1. 项目背景:短信在业务系统里到底承担了什么
1.1 从场景反推需求
很多人一说到“发短信”,第一反应就是发验证码。但实际接进系统之后你会发现,短信的用途远比这个广:
- 用户注册和登录时的验证码,这是最核心的场景;
- 密码找回、手机号换绑时的身份校验;
- 订单状态变更、支付结果、系统告警的通知推送;
- 运营活动里的营销短信。
不同场景对短信的要求不太一样:验证码类看重到达速度和成功率,通知类看重模板规范和发送量,营销类则更关注成本和审核规则。大多数项目最初都是从验证码登录入手的,所以这篇文章会以“手机号+验证码登录”为主线,再往外延伸到通知类和测试、部署、压测这些周边环节。
我当时手里那套业务系统,用户量不大,但对短信的稳定性要求极高。客户的诉求很直接:手机号能收码、登录不卡顿、发送记录可追溯、费用可控。这几个要求看起来简单,真正落地时涉及的东西却不少,包括短信服务商的选型、签名和模板审核、后端接口设计、频控策略、日志埋点,甚至压测时如何避免把短信额度打爆。这些我都会在后文里面逐个展开。
1.2 为什么选了阿里云短信
市面上短信服务商很多,云厂商、短信平台、短信猫一类的第三方都有。我当时对比了一圈,最后定下阿里云,主要基于这几点:
第一,接口稳定,生态完善。阿里云短信服务(Dysmsapi)作为云上标准产品,OpenAPI很规范,SDK覆盖了Java、Python、Go、Node.js等主流语言,文档还算清晰。对于团队来说,能减少学习和接入成本。
第二,和业务系统部署环境能直接打通。如果业务都在阿里云ECS上,短信API走内网或公网都方便,网络延迟和稳定性更有保障。这一点在我们后来做ECS迁移和压测时体现得很明显。
第三,控制台的可视化能力够用。签名、模板的审核状态,发送记录的查询,套餐包的用量统计,都有现成的界面,不用自己再造轮子。
第四,费用透明,按量付费。对于中小规模业务,成本可控,而且有套餐包可以抵扣,对预算敏感的项目比较友好。
当然,阿里云不是唯一选择。如果你只需要极低量的通知短信,或者业务高度依赖某个特定运营商的通道,可能其他服务商更适合。但从通用性和长期维护的角度看,阿里云是很稳的默认选项。
提示:选短信服务商时不要只看单价,还要关注到达率、审核时效、支持的语言和地区、以及是否有专门的客服对接。有些短信平台价格便宜,但审核能卡你两三天,关键时候很耽误事。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开通与准备工作:从控制台到AccessKey,这些细节别忽略
2.1 开通短信服务和购买套餐包
登录阿里云控制台,搜索“短信服务”,进入产品页后按提示开通。这一步本身很简单,但有几个容易被忽略的点:
- 账号需要完成实名认证。个人认证可以申请部分签名,企业认证能申请更规范的签名和更多类型模板。如果项目主体是公司,建议尽早用企业账号认证,不然后面申请签名会卡壳;
- 开通服务后要先检查“套餐包”。短信是按条数计费的,可以先买一个小的套餐包用于测试,避免测试阶段把账户余额烧太多。购买时注意套餐包有效期的限制,有些特价包只有几个月有效期,别囤太多;
- 查看“免费额度”。部分新用户可能有免费短信条数,可以用于功能联调,但开通之前要看清楚额度是否适用于签名模板类型。
我自己的经验是:开通短信服务这件事要提前做,千万不要等代码写完了才去开通。有次项目赶工,我提前把签名和模板申请了,结果客户资料那边出了问题,模板审核卡了两天,差点影响上线计划。后来凡是接短信的项目,我都会把“开通+签名+模板”放到项目启动第一周。
2.2 创建RAM子用户和AccessKey,别用主账号密钥
AccessKey是调用短信API时用来标识身份的密钥,由AccessKey ID和AccessKey Secret组成。很多新手图省事,直接用主账号的AccessKey,这其实非常危险。主账号密钥一旦泄露,等于整个云账号对别人敞开了大门。
正确的做法是:
- 在阿里云控制台进入RAM访问控制;
- 创建子用户,例如“sms-sender”,并且只授予短信发送相关的权限;
- 为该子用户创建AccessKey;
- 在代码或配置文件中使用这个子用户的AccessKey,不要暴露明文到前端或Git仓库。
我踩过的一个坑就是:给子用户配的权限是“AliyunDysmsFullAccess”,但实际调用时却报“NoPermission”,后来才发现RAM里有些默认的SystemPolicy并不包含短信服务的全部操作,需要手动添加自定义授权或选择正确的系统策略。经验是:在RAM授权时,直接搜索“Dysms”或“短信”相关权限,仔细核对之后再保存。
AccessKey的管理还有几个小建议:
- 定期轮换密钥,尤其是有人离职时要及时禁用或删除;
- 使用阿里云KMS或配置中心来管理密钥,尽量不写在配置文件里;
- 如果代码仓库是公开的,记得先检查历史记录里有没有泄露过AccessKey,阿里云控制台有安全告警功能,会提示是否存在泄露风险。
2.3 申请签名和模板:一次过审的实操要点
阿里云短信发送前必须准备好两样东西:签名和模板。
签名就是用户收到短信时显示的发送方名称,比如“【XX云】”。申请的时候根据账号认证类型,需要提交对应的资质材料。个人用户通常可以申请“APP名字”、“网站名字”或“个人名字”等签名;企业用户则可以用企业全称、简称、产品名称等作为签名。
我这边实际申请时的经验:
- 签名的名称要和资质材料一致,不要搞什么“XX客服”这类模糊的名字,审核很可能被打回;
- 申请后状态会经历“审核中 → 通过/拒绝”,一般在几小时到一天内出结果,但高峰期可能要等更久,所以一定要预留时间;
- 如果被拒,控制台会给出原因,常见的比如“签名与实际网站或APP不一致”、“材料模糊不清晰”,按提示修改后重新提交就行。
模板则是短信内容的格式,比如“您的验证码为${code},5分钟内有效。”这里的${code}是一个变量,发送时由接口参数填进去。写模板时有几个规范要特别注意:
- 不能包含敏感词、广告词、促销词,个人开发环境下尤其要注意;
- 变量不能自定义太多,一般一个模板允许的变量数量有限,而且变量名最好用英文字母缩写,比如${code}、${product};
- 同一类场景(验证码、通知、营销)的模板要分开申请,混用容易被拒;
- 模板内容里不要写网址、电话、微信号这些信息,审核严格时会直接拒绝。
我当时为了测试方便,先是申请了一个“验证码”模板,内容就是“您正在登录,验证码为${code},有效期5分钟”,签名用公司简称,材料上传企业营业执照,审核大概两三个小时就通过了。如果你也是企业认证,顺着这个方向准备,通过率通常比较高。
3. 验证码登录功能的完整实现
3.1 需求梳理与表结构设计
验证码登录的流程听上去简单:用户输入手机号 → 后端下发验证码 → 用户输入验证码 → 后端校验并登录。但真正设计时要考虑的点不少:
- 同一个手机号多久可以发一次?
- 验证码有效期多长?
- 验证码错误次数达到上限怎么办?
- 发送记录要不要持久化?
我建议在设计阶段先画清楚这些约束,再建表和写代码。下面是我常用的一张发送记录表结构:
sql复制CREATE TABLE sms_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
phone VARCHAR(20) NOT NULL COMMENT '手机号',
scene VARCHAR(32) NOT NULL COMMENT '场景标识:login/register/forget',
template_code VARCHAR(64) NULL COMMENT '短信模板编码',
sign_name VARCHAR(64) NULL COMMENT '短信签名',
biz_id VARCHAR(64) NULL COMMENT '阿里云返回的业务ID',
code_status TINYINT DEFAULT 0 COMMENT '验证码状态:0未使用 1已使用 2已过期',
request_ip VARCHAR(64) NULL COMMENT '发起请求的IP',
send_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '发送时间',
expire_time DATETIME NULL COMMENT '过期时间',
verify_time DATETIME NULL COMMENT '校验时间',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_phone (phone),
INDEX idx_biz_id (biz_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='短信发送记录表';
这张表的作用不只是记录日志,更重要的是为了排查问题和做统计。比如用户反馈收不到短信,你可以根据手机号查到biz_id,然后去阿里云控制台查具体的发送状态;如果要排查是否被轰炸,也可以按IP和手机号维度快速筛选。
不要把验证码直接只存在数据库里,我会同时把验证码缓存在Redis里,用于登录时的快速校验。Redis的key可以用sms:code:{phone}:{scene},TTL设置5分钟。数据库记录用于审计,Redis用于高频校验,两者分工明确。
3.2 发送验证码接口的实现
发送验证码的核心逻辑分三步:查频控 → 生成验证码 → 调用阿里云API。频控这段等会儿单独聊,先把调用阿里云短信的代码写清楚。
老版本使用aliyun-java-sdk-core和aliyun-java-sdk-dysmsapi的代码在网上流传很广,我这里提供一个比较通用的写法。首先在pom.xml里引入依赖:
xml复制<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.6.3</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.2.1</version>
</dependency>
然后写一个发送工具类:
java复制import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.profile.DefaultProfile;
public class AliyunSmsUtil {
private static final String REGION_ID = "cn-hangzhou";
private static final String ACCESS_KEY_ID = "你的AccessKeyId";
private static final String ACCESS_KEY_SECRET = "你的AccessKeySecret";
private static final String SIGN_NAME = "你的签名";
public static SendSmsResponse sendVerifyCode(String phone, String code, String templateCode) {
DefaultProfile profile = DefaultProfile.getProfile(REGION_ID, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
IAcsClient client = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setPhoneNumbers(phone);
request.setSignName(SIGN_NAME);
request.setTemplateCode(templateCode);
request.setTemplateParam("{\"code\":\"" + code + "\"}");
try {
SendSmsResponse response = client.getAcsResponse(request);
return response;
} catch (Exception e) {
throw new RuntimeException("短信发送失败", e);
}
}
}
这里有两个需要提醒的细节:
第一,REGION_ID 是API的地域节点,一般不需要改成发短信的归属地,默认cn-hangzhou即可,除非你明确知道要用其他地域节点,否则别乱改。
第二,TemplateParam 的格式必须是合法的JSON字符串。如果你的模板里有两个变量,比如${code}和${product},参数就应该是`{"code":"123456","product":"测试"},千万别随手拼字符串,容易出现JSON格式错误,返回isv.INVALID_PARAMETERS。
生成验证码时,我会用String.format("%06d", random.nextInt(1000000)),固定6位数字。位数太少容易暴力破解,太多用户输入麻烦,6位是目前的主流做法。验证码生成后同时写入Redis和数据库,Redis的TTL设为5分钟,数据库的expire_time也存成5分钟后的时间。
3.3 校验验证码并完成登录
用户拿到验证码之后,在登录表单里输入手机号和验证码,后端接口要做的事:
- 校验手机号格式,简单正则匹配;
- 从Redis取验证码,取不到说明过期或不存在;
- 对比输入的验证码和缓存中的值是否一致;
- 一致则标记验证码已使用,执行登录逻辑,不一致则返回“验证码错误”。
这里有一个安全细节:验证码校验要一次性使用。用户输入一次错误后,验证码应该失效或重新生成。否则就给了攻击者暴力枚举的空间。还有一点,避免通过接口返回的差异来判断验证码是否正确,最好统一提示“验证码错误或已过期”,不要告诉用户到底是“错误”还是“过期”。
登录逻辑走完之后,根据你的技术栈去生成会话凭证。如果用的是Spring Boot + JWT,就签发一个Token返回给前端;如果用的是若依这类框架,通常会把Token放入Redis并返回Token串。这部分不复杂,关键是要和现有权限体系串起来,让登录后的请求能正确识别用户身份。
我当时的实现中,登录成功后还会更新sms_log里记录的状态为已使用,把verify_time写入,方便后续对账。这个步骤很多人会忽略,等真正需要翻查记录时就后悔了。
3.4 防短信轰炸:频率限制与风控的完整方案
“短信验证码轰炸”这类问题,做验证码接口的几乎都会遇到。攻击者通过循环请求发送验证码接口,让某个手机号收到大量垃圾短信,或者相反,用别人的手机号给受害者不断发送验证码。不管是哪种,处理不好都会给用户带来极大困扰,还可能导致短信服务被平台暂停。
我在业务层做了三道防线:
第一道,单手机号频控。同一个手机号60秒内只能发一次,一天最多发10次。这里要注意,Redis的递增计数可以设置当天零点过期,但如果跨天时区间没处理好,容易出现统计偏差。我建议用当天的日期作为key的一部分,比如sms:count:{phone}:20250105。
第二道,IP维度限流。同一个IP在一段时间内如果频繁请求发送验证码,直接返回“操作过于频繁”。对于后端接口,可以通过网关层或过滤器里记录request.getRemoteAddr()来实现。但要注意,如果前面挂了Nginx或者SLB,IP可能被代理替换,需要配置好x-forwarded-的方式。
第三道,图形验证码或滑块验证。在发送验证码之前,要求用户先完成滑块或输入图片验证码。这一步能挡住大量自动化脚本。日常登录场景下,用户只多了一步操作,安全提升非常明显。
除了这三道防线,阿里云平台本身也有频控策略,如果超过限制会返回isv.BUSINESS_LIMIT_CONTROL。这个错误码我在第四章会专门讲。你们自己系统的频控尽量在业务层前置拦截,不要把压力全留给平台侧。
4. 短信接口测试:从“不知道是啥”到随手可测
4.1 短信接口测试到底测什么
很多后端新手看到“短信接口测试”这几个字会懵:不就是发个短信吗,有什么好测的?其实短信接口的测试点还挺多的,我简单梳理一下:
- 功能测试:验证码是否能正常发出;接收方手机号是否能正常收到;内容是否正确;验证码能否正确校验;
- 参数校验测试:手机号格式非法、模板参数缺失、模板参数格式错误、签名为空,这些异常情况是否返回合理的错误码;
- 频控测试:同一手机号短时间重复请求,是否被拦截;超过每日上限后是否返回友好提示;
- 依赖测试:如果阿里云服务不可用,后端是否有兜底;返回超时时,接口是否会卡住;
- 安全测试:是否有人频繁枚举验证码;验证码有效期和错误次数是否正确;
- 成本控制测试:大量并发请求时,短信发送量是否被业务层限流限制住,避免短信费用失控。
对照这份清单,你会发现短信接口的测试远不止“能发能收”这么简单。
4.2 用Postman直连OpenAPI做冒烟测试
在代码还没写完的时候,或者想快速验证AccessKey、签名、模板是否可用,可以直接用Postman或ApiPost调用阿里云短信的OpenAPI。这样能先排除配置问题,再回头查代码,排查效率会高很多。
调用阿里云OpenAPI需要先构造签名。如果你用的是Postman,在Headers里填好公共参数,然后用阿里云提供的签名工具生成签名。这里不详细讲手工签名的完整流程,因为比较繁琐,我通常用一个小工具类生成后再贴到Postman里测试。
下面是一组关键的公共参数:
| 参数名 | 示例值 |
|---|---|
| Format | JSON |
| Version | 2017-05-25 |
| AccessKeyId | 你的AK ID |
| SignatureMethod | HMAC-SHA1 |
| SignatureNonce | 每次请求唯一的随机字符串 |
| Timestamp | 2025-01-05T10:00:00Z |
| Action | SendSms |
| Signature | 计算结果 |
请求体里设置PhoneNumbers、SignName、TemplateCode、TemplateParam。如果返回的Code是OK,说明链路是通的。如果是其他错误码,按错误码去排查即可。
注意:Direct调用OpenAPI的手工签名很容易因为SignatureNonce重复、时间格式不对而失败,所以平时测试我更推荐直接用SDK写一个JUnit测试类,或者用阿里云控制台提供的“OpenAPI Explorer”来做调试。OpenAPI Explorer里能直接填参数、看返回,还可以自动生成各语言的调用代码,特别适合初次接触的人。
4.3 在代码层做单元测试与Mock
单元测试的目的是验证自己的业务逻辑,而不是真正去消耗短信资源。我习惯在测试环境里使用Mock,把这个Mock写得很干净:
java复制@MockBean
private SmsClient smsClient;
@BeforeEach
void setUp() {
SendSmsResponse response = new SendSmsResponse();
response.setCode("OK");
response.setBizId("1234567890");
Mockito.when(smsClient.sendSms(Mockito.any())).thenReturn(response);
}
这样测试发送验证码接口时,不会真给手机号发短信,也不会产生短信费用。核心业务逻辑照样覆盖:手机号校验、频控判断、验证码写入Redis、返回成功结果。
对于真正需要走短信通道的联调测试,我建议单独准备一个“测试手机号”,用真实的短信发送验证码。测试环境的签名和模板可以和生产分开,避免误发到真实用户手机上。阿里云控制台中发送记录查询非常方便,联调时可以根据biz_id去对照日志查看发送结果。
5. 若依微服务项目中接入短信登录的改造笔记
5.1 与若依(RuoYi)框架集成的思路
若依是目前国内使用非常广泛的后台管理框架,有单体版、微服务版。我接的项目里,线上环境是基于若依微服务版搭建的,技术栈包括Spring Cloud Gateway、Nacos、Redis、MySQL等。短信登录是在若依默认的用户名密码登录之外扩展出的一个新方式,整体改造的复杂度并不高。
若依微服务版的登录认证链路大致是:
客户端请求 → Gateway网关 → ruoyi-auth模块 → 校验并生成Token。
短信登录要做的就是在auth模块增加一个登录入口。我当时的改造方案是:
- 在AuthController里增加一个
/smsLogin接口,接收phone和code两个参数; - 先调用验证码校验逻辑,检查Redis中是否存在对应该手机号的有效验证码;
- 校验通过后,通过
SysUserService查询手机号对应的用户; - 如果用户不存在,可以选择自动注册或返回“该手机号未注册”;
- 用户存在后,走若依原有的登录成功逻辑,生成Token并返回给前端。
这里有个关键点:若依默认的用户名密码校验在SysLoginService里,生成Token的逻辑在LoginUser和相关Service中,改造时尽量复用原有代码,不要自己另起一套会话体系,否则会有很多坑,比如刷新Token、权限初始化、在线用户列表都兼容不了。
网关层面要注意放行/smsLogin路径,否则请求会被网关拦下来。若依微服务版网关的认证过滤器一般会忽略白名单路径,在配置文件里加上即可。
5.2 公共组件封装与配置的最佳实践
在多个项目里接阿里云短信之后,我越来越倾向于把短信发送能力封装成一个公共starter或工具模块。好处很明显:签名、模板、AccessKey这些都收敛在一块,其他业务模块只调用smsService.sendSms(phone, scene, params)就行,不用关心底层是阿里云还是其他服务商。
我习惯的封装方式是这样的:
java复制public interface SmsService {
SmsResult sendVerifyCode(String phone, String scene);
SmsResult sendNotify(String phone, String templateCode, Map<String, String> params);
boolean verifyCode(String phone, String scene, String code);
}
这个接口后面接一个AliyunSmsServiceImpl,专门处理阿里云相关的逻辑。这样做的好处是:如果将来要切换短信服务商,只有实现类会变,上层业务不用动。我由于吃过“代码里到处new阿里云客户端”的亏,所以现在每个云厂商的SDK都尽量封装在独立的Service层里。
关于配置,我建议把AccessKey、Secret、签名、模板编号和Redis的缓存时间全部放到Nacos或配置中心。不要在代码里写死,也不要写进Git仓库。如果项目还没有配置中心,至少也要放到application.yml里,并且用环境变量占位,比如:
yaml复制aliyun:
sms:
access-key-id: ${SMS_ACCESS_KEY_ID}
access-key-secret: ${SMS_ACCESS_KEY_SECRET}
sign-name: ${SMS_SIGN_NAME}
verify-code-template: ${SMS_VERIFY_CODE_TEMPLATE}
在部署时通过环境变量注入,就算运维不小心把配置文件发到群里,也不会直接泄露密钥。
5.3 迁移到阿里云ECS时的短信适配
我做过一个从自建机房往阿里云ECS迁移的项目,迁移对象是一套单节点k8s上跑的若依微服务环境。迁移要求是“准不停服、不丢数据”,迁移完成后还要用JMeter脚本做高并发压测来验证云上承载能力。
这个过程中,短信服务这块的处理让我印象很深。很多人以为迁移ECS之后短信也要跟着“迁移”,其实不是的。阿里云短信本身就是云上服务,不管你的业务部署在哪台ECS上,调用方式和配置完全不用变。真正需要关注的是以下几点:
一,网络出口。短信API的调用走的是HTTPS,ECS只要能够访问公网即可。迁移之前先确认安全组策略没有把出方向的443端口禁掉,不然代码跑起来之后短信发送会在网络层超时。我当时遇到过安全组只放行了80,结果SSL证书和短信API全连不上的情况。
二,密钥不要跟着配置文件一起搬。迁移时如果只是简单地把代码打包搬过来,AccessKey也以明文躺在配置里,那就有泄露风险。我建议迁移后的第一件事就是检查代码仓库和环境变量,确认没有把AccessKey、数据库密码这类敏感信息写进去。
三,时区问题。很多服务器默认时区是UTC,如果你在日志中记录发送时间,会和阿里云控制台的发送记录差8个小时。虽然不对业务造成直接影响,但排查问题时很容易被误导。迁移时记得把ECS的时区设为Asia/Shanghai。
四,压力测试时的短信用量。JMeter高并发压测时,测试脚本如果涉及到发短信接口,会瞬间产生大量短信请求。一方面会产生费用,另一方面会触发平台频控,还会让测试机收到大量垃圾短信。压测前建议把短信发送这个动作改成mock逻辑,只测登录校验链路,不真的请求云短信API。等压测结束再打开真实短信链路核对线上配额即可。
这些细节听起来不大,但在迁移项目中都是实打实的坑。我们当时压测脚本里带了短信登录的接口,还好提前做了降级处理,不然几分钟就能把当月的短信套餐包消耗大半,那叫一个心疼。
6. 常见问题与排查技巧实录
6.1 高频错误码排查速查表
下面这个表格里的内容,是我在实际项目里遇到过的、或者团队里其他同事踩过坑后汇总出来的,基本覆盖了最常见的短信发送异常。
| 错误码 | 含义 | 排查思路 |
|---|---|---|
| OK | 成功 | 返回的bizId可以用于后续查询发送状态 |
| isv.SMS_SIGNATURE_ILLEGAL | 签名不合法或未审核 | 登录控制台查看签名是否通过审核;检查传入的签名是否和申请的一致 |
| isv.SMS_TEMPLATE_ILLEGAL | 模板不合法或未审核 | 检查模板编号是否填写错误;模板是否仍处于审核中状态 |
| isv.MOBILE_NUMBER_ILLEGAL | 手机号不合法 | 检查号码格式,不要带+86前缀,也不要包含空格和横线 |
| isv.INVALID_PARAMETERS | 参数格式错误 | 重点检查TemplateParam是否是正确的JSON,逗号、引号、花括号是否完整 |
| isv.BUSINESS_LIMIT_CONTROL | 触发平台流控 | 说明发送频率超出平台默认限制;建议在业务层增加频控,或等待一段时间再试 |
| SignatureDoesNotMatch | 签名计算错误 | 检查AccessKeyId和AccessKeySecret是否正确;SignatureNonce是否每次唯一;服务器时间是否准确 |
| Throttling | 请求太频繁 | 这个一般是SDK客户端请求过多导致,适当加长调用的间隔时间 |
| TemplateParameterCountIsNotMatched | 模板变量个数不匹配 | 对比模板中定义的所有变量,是否每一个都在TemplateParam里传入了值 |
排查时我有个习惯:把发送记录、Redis日志、阿里云控制台的发送记录一起打开,同一个手机号同一个时间段对比着看。哪一层出了问题就一目了然。比如Redis里验证码还在,但用户说没收到短信,那就去阿里云控制台查bizId对应的状态,看是“成功”还是“失败”,失败原因是什么。宁可多花一分钟看日志,也好过瞎猜。
6.2 验证码收不到的5个隐藏原因
收不到短信验证码是最常见的问题,很多时候并不是阿里云API返回了错误,而是链路里的某个环节静默失败了。我这里整理几个容易忽略的原因:
第一,签名被运营商拦截。短信虽然提交成功,但内容触发了运营商策略,比如含有“验证码”、“链接”等字样,或签名不规范,可能导致延迟到达或直接拦截。这种情况通常在阿里云控制台看不到明显异常,只能减少营销类词汇、确保签名和模板合规。
第二,手机号错绑了。用户填写的手机号不是自己当前使用的号码,或者手机系统把短信拦截到垃圾箱里了。让用户检查一下骚扰拦截和垃圾短信目录,能解决一大部分“没收到验证码”的投诉。
第三,验证码在Redis和数据库里不一致。我在并发场景下遇到过,代码生成验证码后先写Redis,再写MySQL,然后写入失败抛异常,但前端已经提示“发送成功”,用户用Redis里的验证码来登录没问题,但在数据库记录里找就会对不上。所以顺序很重要,我建议先把验证码写入Redis并确认成功,再往数据库落日志,任何一步失败都要向前端返回“发送失败”。
第四,使用相同验证码请求了多次。如果你对同一个手机号重复发送,后端没有将前一个验证码清掉,用户收到的是两条不同的验证码,只有最后一条有效。这种情况要在发送新验证码时主动把旧验证码覆盖掉,不要让两个验证码共存。
第五,测试环境里的验证码被mock掉了。开发环境为了省短信费,有可能写了“万能验证码123456”来绕过真实发送,结果忘了在生产环境去掉。这种情况如果出现在正式环境,安全隐患非常严重,一定要在部署检查清单里加一条:生产环境禁用mock验证码。
6.3 压测和上线时的短信容灾建议
最后聊一下“准不停服”迁移和压测过程中,我总结出来的几条经验:
一是给短信发送接口做一个“熔断开关”。可以通过配置中心动态开启或关闭短信发送,平时开启,压测时一键关闭,让接口直接返回成功但不真正调用短信API。这个开关在迁移验证、联调、演示、压测这些场景下救了我不少次。
二是对短信发送失败要有补偿机制。如果阿里云接口超时或返回异常,业务层至少要做到重试一次,并把失败记录落库或发到监控告警,而不是直接吞掉异常。用户感知应该是“稍后重试”,而不是“成功了但没收到”。
三是监控告警要前置。我比较习惯把短信API的响应时间、成功率、失败码数量做成指标,配到监控平台上。一旦某个错误码开始突增,比如isv.BUSINESS_LIMIT_CONTROL持续出现,就要提醒运营和技术人员去处理。短信这事儿看起来很基础,一旦出问题,影响的是所有登录用户的体验,优先级并不比核心业务功能低。
四是在高并发压测时,JMeter脚本里最好把“发送验证码”和“登录校验”分开压。发送验证码涉及外部API,延迟高且受频控影响;登录校验是纯内部逻辑,压出来的数据更能反映系统本身的能力。压测结束后再挑一个低峰时段,做几次真实发送验证码的冒烟测试,确保链路依然健康。
最后再分享一个我自己的体会吧。阿里云短信的接入门槛真的不高,花半天看完文档、调通一个接口并不难。真正拉开差距的,是对签名模板申请节奏的掌控、对频控和安全的重视、对错误码和日志的熟悉程度。我见过太多项目在最后关头才想起申请签名,结果审核一卡就是一天,也见过因为AccessKey泄露导致损失惨重的案例。无论项目大小,我建议你把签名模板申请、RAM权限最小化、发送失败告警这三件事排在优先位置,短信这件事就不会给你拖后腿。
