做后端这几年,短信这块真是绕不开。注册登录要验证码,订单状态要通知,服务器出问题要告警,十有八九都得靠短信顶上。前阵子我把一个老项目的短信通道整体梳理了一遍,最终定的方案就是阿里云短信服务。这篇“阿里短信使用案例”就把我实际走通的一整套流程写出来,从申请签名、配置模板、写后端代码,到接口测试、排查各种报错,全串起来讲一遍。如果你正准备给系统接入短信,或者还在纠结要不要把短信猫换掉,这篇应该能直接给你省下不少试错成本。
1. 选型逻辑:为什么最终落在阿里云短信上
1.1 项目里常见的短信需求
先别急着谈技术,得先想清楚你的系统到底需要短信干什么。我在实际项目里总结下来,大部分场景可以归成四类。
第一类是身份验证类,典型代表就是短信验证码。注册、登录、修改密码、二次校验,这类短信对时效性要求极高,用户点了“获取验证码”,几秒钟之内没收到就会开始焦虑,超过一分钟基本就要投诉了。第二类是系统通知类,订单状态变更、物流提醒、服务到期提醒,这类短信内容固定,一般用模板消息就够了,对实时性要求稍低一些,但也不希望延迟太久。第三类是告警类,服务器CPU飙高、磁盘快满了、定时任务失败,这类短信是发给运维和开发自己的,量不大,但要求稳定可靠。第四类是营销类,活动推广、召回通知,这类短信频控要求高,发多了容易被投诉甚至被封。
不同的场景对短信通道的要求完全不一样。验证码要快,通知要稳,营销要合规。选型的时候不可能一套方案包打天下,但云短信是这四个场景里覆盖度最高的。
1.2 短信猫、自建通道与云短信怎么选
早年很多中小项目喜欢用短信猫,就是那种插SIM卡的硬件设备,接在服务器上通过串口或者USB发短信。短信猫的好处是初期成本低,一条短信的费用基本就是SIM卡套餐的钱,而且数据完全在自己手里。但真正拿它做生产依赖,问题就来了:设备得一直在线,电脑重启或者USB松动都可能断发;SIM卡欠费或者被运营商风控,短信直接发不出去,你还得手工去换卡;没有标准的回执机制,用户到底收没收到,你根本不知道。我见过一个项目用短信猫发验证码,结果某次运营商把那个号段的风控策略调严了,整整一个下午用户都收不到验证码,排查了半天才发现是卡被限制了。
自建运营商通道更不用说了,需要SP资质、需要和运营商对接协议、需要自己维护收发链路,一般公司根本扛不住这个成本。云短信的优势就在于把通道、资质、回执这些事情全都外包了,你只需要做好API接入。阿里云短信服务在我这边的几个核心优势是:SDK维护得比较勤,文档结构清晰,控制台能直接看到发送量和消费明细,而且回执查询做得很完善。下面这张表可以直观看出差异:
| 对比项 | 短信猫 | 自建运营商通道 | 阿里云短信 |
|---|---|---|---|
| 硬件与资质 | 需要硬件,插SIM卡 | 需要SP资质,成本高 | 实名认证即可,无硬件 |
| 到达率 | 受SIM卡状态影响 | 取决于通道质量 | 运营商直连,稳定 |
| 回执 | 基本没有 | 需自己对接 | 支持查询与推送 |
| 维护成本 | 设备故障需现场处理 | 高 | 低 |
| 接入方式 | 串口/USB/SDK | 运营商协议对接 | HTTP API,SDK成熟 |
所以我的结论是:除非你只是做个内部测试工具、一天发个几十条,否则云短信基本是唯一合理的选择。阿里云在这块的市场份额和技术成熟度都比较靠前,踩坑的概率相对小。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 接入前必须搞懂的三个基础概念
2.1 签名与模板:短信能不能发出去的第零道门槛
很多人第一次接触阿里云短信时,容易忽略一个关键点:不是你在控制台开通了短信服务就能直接调API发短信。你必须先申请短信签名和短信模板,两项都审核通过后,才能真正把短信发出去。
短信签名就是用户收到的短信里那个【】里的内容,比如“【某科技】您的验证码是123456”,这里的“某科技”就是签名。签名代表发送方的身份,不是什么词汇都能用的。个人认证用户能申请的签名类型和额度有限,企业认证会宽松很多,比如可以用公司名称、App名称、小程序名称、公众号名称等。申请时需要提交对应的资质材料,比如营业执照、App备案截图、应用商店上架截图等。审核周期一般不长,快的几分钟,慢的几个小时,但如果是第一次提交,建议预留半个工作日以上。
短信模板就是短信正文的框架,里面用${}占位符表示动态内容。比如验证码模板可以是:“您的验证码为${code},${minute}分钟内有效。请勿泄露给他人。”这里的${code}和${minute}就是变量,每次调用API时通过TemplateParam参数传入实际值。模板的审核规则比签名更细,比如不能包含“测试”字样,不能包含外部链接、邮箱、微信号,不能使用不确定的词语比如“随时”“最后一天”等营销敏感词。我第一次提交模板时,文案里写了一个官网链接用于“查询详情”,直接被驳回,改成纯文字后才通过。后来我学乖了,模板申请这件事一定放在项目开发的第一周做,不能拖到上线前一天。
2.2 AccessKey 与 RAM 子账号:别把主账号钥匙交给代码
调用阿里云OpenAPI需要AccessKey,也就是AccessKey ID和AccessKey Secret这一对密钥。很多老教程会直接让你登录主账号,然后去控制台创建AccessKey,但这其实是非常危险的做法。主账号的AccessKey拥有账号下的所有权限,万一泄露,等于把整个云资源都暴露了。我之前接手过一个项目,代码里硬编码了主账号AK,后来发现那台测试服务器被扫描过,吓得当场就轮换了密钥。
正确做法是创建一个RAM子账号,只给它开通短信服务相关的权限,比如AliyunDysmsFullAccess策略,然后把子账号的AccessKey给到应用使用。这样即使AK泄露,影响范围也被限制在短信服务内。如果你用的是K8s或者ECS部署,建议把AK放到环境变量、配置中心或者KMS里,别写死在代码仓库。另外要记住,AccessKey Secret只在创建时完整展示一次,后面再查只能重新创建;如果怀疑泄露,直接禁用旧的并生成新的。
3. 服务端接入实操:Spring Boot 项目发一条短信
3.1 先说依赖:Maven 仓库配置与版本选择
阿里云短信服务有Java SDK,接入方式分旧版和新版。旧版的典型依赖是aliyun-java-sdk-core加aliyun-java-sdk-dysmsapi,网上很多老教程都是这种。新版则是独立的dysmsapi20170525,代码风格更现代化,推荐新项目使用。
无论用哪个版本,第一步都是从Maven仓库拉依赖。国内环境直接拉Maven中央仓库经常慢得让人崩溃,所以通常会把Maven的镜像源配置成阿里云公共仓库。在maven的settings.xml里加一段mirror配置就行:
xml复制<mirrors>
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
</mirrors>
mirrorOf配成*表示所有仓库请求都走这个镜像,如果你只想覆盖中央仓库,也可以配成central。拉SDK的时候,新版SDK加这几个依赖:
xml复制<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>2.0.24</version>
</dependency>
最好在阿里云官方文档里查一下最新版本号,Maven坐标的版本更新比较频繁,旧版本可能存在一些已经修复的bug。如果你用的还是Spring Boot老版本,SDK的Java版本兼容性也要看一眼,别拉进来之后编译报错。
3.2 写代码:新版 SDK 发送短信的完整示例
新版SDK的初始化方式和老版差别挺大。老版需要先构造DefaultProfile,再通过IAcsClient发送;新版直接构造Client对象,代码更简洁。我一般会把短信相关配置放到application.yml里:
yaml复制aliyun:
sms:
access-key-id: your-access-key-id
access-key-secret: your-access-key-secret
sign-name: 你的签名名称
template-code: SMS_123456
然后在配置类里读取并初始化客户端:
java复制@Configuration
@ConfigurationProperties(prefix = "aliyun.sms")
public class SmsConfig {
private String accessKeyId;
private String accessKeySecret;
private String signName;
private String templateCode;
// getter / setter 省略
}
接着封装一个SmsService,核心发送方法长这样:
java复制@Service
public class SmsService {
private final com.aliyun.dysmsapi20170525.Client client;
private final String signName;
private final String templateCode;
public SmsService(SmsConfig config) {
com.aliyun.teaopenapi.models.Config teaConfig = new com.aliyun.teaopenapi.models.Config()
.setAccessKeyId(config.getAccessKeyId())
.setAccessKeySecret(config.getAccessKeySecret());
teaConfig.endpoint = "dysmsapi.aliyuncs.com";
this.client = new com.aliyun.dysmsapi20170525.Client(teaConfig);
this.signName = config.getSignName();
this.templateCode = config.getTemplateCode();
}
public SendSmsResponse sendVerifyCode(String phone, String code) throws Exception {
// 模板参数必须是一个JSON字符串,哪怕只有一个变量
String templateParam = "{\"code\":\"" + code + "\"}";
com.aliyun.dysmsapi20170525.models.SendSmsRequest req =
new com.aliyun.dysmsapi20170525.models.SendSmsRequest()
.setPhoneNumbers(phone)
.setSignName(signName)
.setTemplateCode(templateCode)
.setTemplateParam(templateParam);
return client.sendSms(req);
}
}
发送成功后的响应对象里,关键字段是Code和Message。Code为OK才代表请求被受理成功,注意这里说的是“受理”,不是“用户已收到”。如果Code不是OK,就要把RequestId、Code、Message都记录下来,去控制台查详细原因。
如果你维护的老项目还在用旧版SDK,也不用急着全部重写。旧版写法大致是这样:
java复制DefaultProfile profile = DefaultProfile.getProfile(
"cn-hangzhou", accessKeyId, accessKeySecret);
IAcsClient client = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setPhoneNumbers(phone);
request.setSignName(signName);
request.setTemplateCode(templateCode);
request.setTemplateParam("{\"code\":\"" + code + "\"}");
SendSmsResponse response = client.getAcsResponse(request);
功能上差别不大,但新SDK的异常处理更友好,推荐新代码统一走新版。无论哪种方式,AccessKey都不要硬编码在代码里,一定通过配置注入。
3.3 工程化细节:校验、日志、线程池与重试策略
代码能发短信只是第一步,真正上线还差几个工程化细节。
首先是对手机号的校验。国内手机号一般用正则^1[3-9]\\d{9}$做前置校验,避免无效号码白白消耗短信配额。如果业务有海外用户,还要单独处理国际区号,阿里云短信对国际号码有另外的调用方式,不能直接用国内的手机号字段。
其次是模板参数的构造。TemplateParam需要的是JSON字符串,只有一个变量的时候手工拼字符串还能接受,变量多了就容易被转义符坑到。踩过坑后我一般都用JSON库序列化:
java复制Map<String, String> paramMap = new HashMap<>();
paramMap.put("code", code);
paramMap.put("minute", "5");
String templateParam = JSON.toJSONString(paramMap);
然后是日志。短信发送的日志一定要单独记录,至少包含手机号、模板Code、模板参数、返回Code、RequestId、耗时。手机号建议做脱敏处理,比如138****0000,既方便排查问题,也符合数据安全习惯。
重试策略也要想清楚。网络抖动导致的超时是常态,但并不是所有失败都适合重试。如果是业务限流错误,比如isv.BUSINESS_LIMIT_CONTROL,重试只会加剧问题;如果是SDK客户端异常或者网络超时,可以重试一两次。验证码场景下用户如果反复点击“获取验证码”,后端也要做幂等控制,防止同一手机号短时间内被重复发送。
4. 短信登录与验证码:完整可落地的业务实现
4.1 从“获取验证码”到“登录成功”的全链路
前面讲的是单条短信能发出去,但真正做短信验证码登录,还需要一套完整的业务逻辑。我拿一个典型的手机号验证码登录来拆解。
用户在前端输入手机号,点击“获取验证码”。前端一般会先做一个图形验证码或者滑块验证,挡住自动化脚本,降低被刷风险。通过后,前端调用后端接口POST /api/auth/sms-code,携带手机号。后端收到请求后,第一步不是发短信,而是先查频控,看这个手机号在最近60秒内有没有发过验证码;如果发过,直接拒绝并提示“操作太频繁”。第二步用SecureRandom生成6位随机数字验证码,注意别用普通的Random类,SecureRandom能避免可预测性问题。第三步把验证码存到Redis里,key设计成sms:code:login:{phone},value就是验证码,TTL设为300秒。第四步才调用短信服务发送,发送成功后再把该手机号的频控key写入Redis,TTL 60秒。
用户收到验证码后,在前端输入,提交到登录接口POST /api/auth/login-by-sms。后端先从Redis里取出之前存的验证码,如果不存在直接返回“验证码已过期”,如果存在则比对是否一致。比对通过后,立刻删除这个key,防止验证码被重放使用,然后签发登录token。这里有个小细节:验证码错误次数也要做限制,比如同一手机号最多错5次,超过后该验证码立即作废,必须重新获取。这么做主要是防暴力枚举,因为验证码只有6位数字,如果没有次数限制,理论上100万次以内一定能试出来。
核心的发送验证码方法可以这样写:
java复制public void sendLoginCode(String phone) {
// 1. 频控检查
String limitKey = "sms:limit:send:" + phone;
if (redisTemplate.hasKey(limitKey)) {
throw new BizException("发送过于频繁,请稍后再试");
}
// 2. 生成验证码并缓存
String code = String.valueOf(new SecureRandom().nextInt(900000) + 100000);
String codeKey = "sms:code:login:" + phone;
redisTemplate.opsForValue().set(codeKey, code, 5, TimeUnit.MINUTES);
// 3. 调用阿里云短信发送
try {
smsService.sendVerifyCode(phone, code);
redisTemplate.opsForValue().set(limitKey, "1", 60, TimeUnit.SECONDS);
} catch (Exception e) {
// 发送失败要删掉验证码,不然用户会拿一个发不出去的码去登录
redisTemplate.delete(codeKey);
throw new BizException("短信发送失败,请稍后再试");
}
}
4.2 防“短信验证码轰炸”的实操配置
“短信验证码轰炸”其实就是攻击者利用接口,对同一个手机号或者一批手机号高频下发验证码,造成骚扰和短信费用损失。这是所有验证码类接口都要面对的常见风险,我们不讨论攻击手法,只说防御方案。
我一般会上这么几层防御。第一层是图形验证码,用户点击“获取验证码”之前必须先通过滑块或者图片验证码,这能挡掉大量纯脚本的机器请求。第二层是后端频控,同一手机号60秒内只能发一次,同一IP每小时限制发送次数,同一设备ID每天限制数量。第三层是业务风控,如果某个手机号在短时间内触发了多次“获取验证码”,即使频控key没到期,也要主动拒绝并记录告警。第四层是对接阿里的云盾验证码服务,成本稍高一些,但对大流量的C端产品是值得的。
另外一个容易被忽略的点是:短信模板本身也要配合。验证码类模板里的文案不要带有诱导性质的词汇,比如“点击链接领取”“免费赠送”这类词容易触发运营商的风控策略,导致短信被拦截率上升。模板简洁规范,发送成功率会明显好一些。
4.3 发送成功不等于已送达:用回执判断真实状态
很多人在联调时会遇到一个情况:API返回的是OK,但用户说没收到短信。这时候就要意识到,SendSms返回OK只代表阿里云已经受理请求,不代表运营商的网关已经把短信送到用户手机上了。中间还隔着运营商通道、用户手机安全软件拦截、伪基站干扰等多个环节。
早期业务量不大时,可以直接用阿里云的QuerySendDetails接口主动查询某条短信的发送详情。调用时传手机号、日期、页码等参数,返回结果里能看出发送状态和状态描述。比如状态为DELIVERED表示已送达,为UNDELIVERY表示未送达,为UNKNOWN表示状态未知。这个接口适合做手工排查,不适合在用户每次请求时同步调用。
当业务量增长到每天上万条,建议配置短信发送回执的推送。阿里云支持把短信状态回执通过MNS消息队列或者HTTP推送的方式发给你的服务端,可以实现真正实时掌握每条短信的最终状态。配置方式是提交回执消息接收地址,然后在服务端暴露一个接收回执的接口,解析JSON消息体。收到回执后更新本地数据库的短信发送记录,这样第二天给业务方看“到达率”就有了数据支撑。
5. 短信接口测试与高频报错排查
5.1 还没写代码时怎么快速验证配置
很多时候接入短信服务,第一个问题不是代码,而是签名和模板是否真的配置好了。这时候不建议直接写代码去试错,可以先用阿里云控制台里的OpenAPI Explorer工具,网页上直接填参数,手动触发一次请求。
OpenAPI Explorer的好处是能看到整个API的请求参数、签名计算过程、响应报文。如果返回结果里签名验证失败,它能帮助你快速锁定是AccessKey的问题还是签名串的问题。比如我自己调试时,经常把AccessKey Secret复制错一个字符,在Explorer里立刻就能发现SignatureDoesNotMatch,不用等到代码跑起来再猜。
更大范围的联调,可以配合阿里云的“测试签名”和“测试模板”。控制台里提供了系统预设的测试签名和模板,不需要审核就能用来验证API调用流程。但测试模板的内容、长度有限制,只能用来验证能发出去,不能用它测试线上文案。真正上线后,一定要在代码里配置正式的签名和模板Code。
如果你想用curl直接调API,不是不行,但需要自己计算HMAC-SHA1签名,比较繁琐。我更推荐直接用官方SDK,或者用APIExplorer生成对应语言的SDK调用示例,复制到项目里微调一下就能用。
5.2 高频错误码速查:照着排查比自己瞎猜快
接入阿里云短信,会遇到的报错其实很固定。下表是我在实际项目中遇到并排查过的高频错误码:
| 错误码 | 含义 | 处理方式 |
|---|---|---|
| SignatureDoesNotMatch | 签名验证失败 | 检查AccessKey ID/Secret,确认系统时间是否准确(偏差超15分钟会失败) |
| InvalidAccessKeyId.NotFound | AccessKey不存在 | 确认AccessKey是否存在、是否被禁用 |
| isv.SMS_SIGNATURE_ILLEGAL | 签名不合法或未审核通过 | 检查签名内容是否与申请的完全一致(包括【】符号),确认审核状态 |
| isv.SMS_TEMPLATE_ILLEGAL | 模板不合法或未审核通过 | 检查TemplateCode是否正确,模板审核是否通过 |
| isv.MOBILE_NUMBER_ILLEGAL | 手机号不合法 | 确认号码是纯数字,不要带+86前缀,检查号码长度 |
| isv.BUSINESS_LIMIT_CONTROL | 触发业务流控限制 | 同一手机号同一模板发送过于频繁,降频或延长间隔 |
| isv.DAY_LIMIT_CONTROL | 触发日发送量限制 | 检查账户日限额,必要时申请提高配额 |
| isv.AMOUNT_NOT_ENOUGH | 账户余额不足 | 充值后再发送 |
| InvalidTimeStamp.Expired | 请求时间戳过期 | 检查服务器时间和时区配置 |
其中isv.SMS_SIGNATURE_ILLEGAL和isv.SMS_TEMPLATE_ILLEGAL这两个错误占了我在实际排查中超过一半的比例,原因基本都是同一类:配置文件和控制台里的名称、Code不一致。有可能是复制错了,有可能是多了个空格,也有可能是改了模板但代码里没同步更新。检查时直接打开控制台对照着看,比反复查日志快得多。
5.3 一个完整的排查思路
当线上反馈“短信发不出去”时,我一般按照下面这个顺序排查。首先看日志里有没有记录RequestId、Code、Message,大多数问题在这一步就能定位。然后拿RequestId去阿里云控制台的短信服务“发送详情查询”里看具体记录,那个页面能看到请求是否被受理、被拒绝的具体原因。如果API返回OK但用户没收到,让用户在手机上检查垃圾短信拦截,同时通过QuerySendDetails或者回执推送查最终状态。
如果确认是签名或模板问题,去控制台看审核状态和驳回原因,阿里云在驳回时通常会给明确原因,比如“模板中含有营销敏感词”。最后,如果怀疑是账户问题,去费用中心看余额,短信服务是后付费,账户欠费会导致发送失败。整个排查链路用熟了,基本十分钟内能定位问题。
6. 从短信猫迁移到云短信的一次真实切换记录
6.1 为什么最终还是放弃了短信猫
老项目之前一直用短信猫发通知,运维同事每隔一段时间就要去机房里看设备状态。后来业务量上来,短信猫的瓶颈彻底暴露了:SIM卡套餐里的短信数量不够用,临时加卡又要去营业厅办;通道不稳定,用户收不到验证码的投诉开始增多;最关键的是没有回执,每次出问题都只能靠用户反馈反推。最终决定切到阿里云短信,核心诉求就是稳定和可观测。
云短信接入后,最直观的变化是发送时间从原来的“看运气”变成了稳定秒级到达,后台能实时查看发送记录和失败原因。成本方面,虽然单条短信价格比短信猫的套餐价略高,但算上人力维护成本,整体反而更划算。
6.2 怎么做到不停服平滑切换
切换这种基础服务,最忌讳一步到位直接换。我当时的做法是抽象出一个ISmsSender接口,旧实现走短信猫,新实现走阿里云,然后通过配置中心控制生效策略。配置成0%的时候全走旧通道,配置成10%的时候开始灰度,内部测试手机号优先走新通道,观察两天没问题后逐步放大比例,最后全量切换。
这个方案的另一个好处是方便回滚。灰度过程中如果发现阿里云这侧有什么问题,把配置改回0%就能立回老通道,整个过程不需要重新发版。短信猫通道在云短信稳定运行一个月后,我才正式下线,原因是所有短信都有对账任务在跑,确认两边状态一致后,短信猫的生命周期才算彻底结束。
如果项目本身是若依脚手架这类工程,或者微服务拆分比较细,更建议把短信发送封装成一个独立的公共模块或者公共服务,不要在每个业务模块里各自接入SDK。统一封装后,后续要换服务商、加频控、加审计,都只改一个地方就够了。
最后分享一个我个人的小习惯。短信这类看似简单却影响用户体验的功能,我一直坚持四个字:可观测。凡是有短信发送的服务,我都会把发送请求、返回Code、RequestId、耗时都打到一个单独的日志文件里,再配一个失败告警。别小看这个动作,很多线上问题都是靠它提前发现的,而不是等用户找上门来。
另外,模板审核这件事真的别卡在最后一天才去申请。我第一次用阿里云短信时,就是上线前一天提交模板,结果文案里带了外部链接被驳回,来回折腾了两轮,差点影响发布时间。把这部分前置到项目开发的第一周,能省掉很多不必要的焦虑。
