Spring Boot集成阿里云短信接口:SendSms开发实战与避坑指南 简介阿里云短信接口开发示例面向需要接入用户验证、通知推送等场景的后端服务开发者集中解决短信服务的快速接入与稳定调用问题。示例完整覆盖了阿里云账号注册、AccessKey申请、SDK与接口文档阅读、HTTP请求构造与响应解析等关键环节并给出清晰的参数说明和可复用代码片段。压缩包共92个文件以C#源文件68个.cs、程序集18个.dll和项目配置文件为主附带说明文档与示意图片整体仅485KB结构精炼便于按需查阅。目前已有579人学习下载比较适合刚接触阿里云短信服务或希望快速落地的开发者。示例对SendSms接口的核心参数如Action、PhoneNumbers、SignName、TemplateCode、TemplateParam进行了逐一讲解同时补充了网络异常、超时重试、发送频率限制和内容审核等实战注意事项能够帮助理解完整调用链路降低实际接入中的排错成本。1. 阿里云短信接口开发先分清直连OpenAPI和云MAS平台两条路很多团队第一次接阿里云短信第一反应是去控制台找“API文档”。打开控制台会看到两套入口直连短信服务的OpenAPI和渠道性质的云MAS平台HTTP接口文档。我第一次做的时候就把两者混了照着云MAS的Java例子往Spring Boot工程里粘贴最后发现鉴权算法完全不是一回事短信api发不出去又不知道看哪里。这里我直接说结论常规的验证码、通知类短信走阿里云官方短信OpenAPI配好AccessKey、审核通过的签名、审核通过的模板用SendSms接口就能发。下面按这个链路给你一套能直接落地的例子从最小工程到线上避坑适合做短信验证码、订单通知、活动营销推送的Java后端工程师尤其是已经上了Spring Boot的团队。2. Spring Boot接入阿里云短信的最小工程SDK选型与SendSms实现2.1 依赖与镜像仓库dysmsapi20170525 为什么更省心Java后端接入阿里云短信常见做法不是手写HTTP签名而是用阿里云官方认证SDK。老一点的项目还在用aliyun-java-sdk-core那一套新项目更推荐直接用dysmsapi20170525这个包命名规范、链式调用、错误信息也更完整。我自己选型时只看两点一是官方还在维护二是不用自己拼AcsRequest的签名串。一旦自己拼哪一步错了比如换行符、URL编码顺序不对调试起来非常痛苦容易掉进“玄学排查”。pom.xml里加依赖之前建议先把Maven仓库地址配置成阿里云仓库。这不是项目必需但国内网络环境下从中央仓库拉最新SDK经常超时换成阿里云仓库镜像后基本秒下。配置方式是在settings.xml里加mirror或者用Spring Boot项目的repositories节点构建地址统一走https://maven.aliyun.com/repository/public。这一步能让整个开发过程少很多无意义的等待。dependency groupIdcom.aliyun/groupId artifactIddysmsapi20170525/artifactId version2.0.24/version /dependency版本号建议以Maven仓库里最新的稳定版为准。2.0.24是我常用过的一个版本如果当前工程里已经有其他阿里云SDK保持版本在同一个大版本内避免tea-openapi的兼容性问题。这个包会连带引入com.aliyun:tea-openapi等基础库如果公司有私有仓库要把依赖都放行。2.2 用RAM子账号建AccessKey别拿主账号Key冒险开发阿里云短信接口前先做权限规划。我一般不建议把主账号AccessKey直接写进配置主账号Key权限范围太大一旦泄露别人可以拿去调用其他云产品。规范做法是在RAM访问控制里创建一个子账号只授予短信服务权限至少配齐下面三项允许签名和模板的只读权限方便在代码里校验。允许发送短信的权限对应Action是dysms:SendSms。允许查询发送详情的权限对应Action是dysms:QuerySendDetails。在控制台创建RAM用户时选择“编程访问”方式生成AccessKey ID和AccessKey Secret。权限策略可以直接选系统自带AliyunDysmsFullAccess也可以按上面的Action自定义JSON策略。生产环境我建议用自定义策略即使这个Key泄露也只能发短信和查短信不能碰ECS、OSS等其他资源。创建好之后把AccessKey放到环境变量或秘密管理服务里不要硬编码在application.yml提交到Git仓库。用环境变量注入是最快的做法后面再换KMS或Vault也容易。这里顺便提一句短信发送结果有时看起来是权限问题实际上是签名和模板归属于另一个账号。RAM用户虽然具备API权限但它只能操作当前账号下的签名和模板账号间资源不能混用。2.3 最小可运行代码从Config到SendSms下面是一个Spring Boot工程里最小可用的发送代码核心是把阿里云SDK的Client初始化交给Spring容器然后在Service里调用SendSms接口。Configuration ConfigurationProperties(prefix aliyun.sms) public class SmsProperties { private String endpoint; private String accessKeyId; private String accessKeySecret; private String signName; private String templateCode; // getter setter 省略Spring Boot 会自动绑定 }Service public class SmsService { private final com.aliyun.dysmsapi20170525.Client client; private final SmsProperties smsProperties; private final ObjectMapper objectMapper; public SmsService(SmsProperties smsProperties) throws Exception { this.smsProperties smsProperties; com.aliyun.teaopenapi.models.Config config new com.aliyun.teaopenapi.models.Config() .setAccessKeyId(smsProperties.getAccessKeyId()) .setAccessKeySecret(smsProperties.getAccessKeySecret()); config.setEndpoint(smsProperties.getEndpoint()); this.client new com.aliyun.dysmsapi20170525.Client(config); this.objectMapper new ObjectMapper(); } public SendSmsResponse send(String phoneNumbers, String templateParamJson) throws Exception { SendSmsRequest request new SendSmsRequest() .setPhoneNumbers(phoneNumbers) .setSignName(smsProperties.getSignName()) .setTemplateCode(smsProperties.getTemplateCode()) .setTemplateParam(templateParamJson); SendSmsResponse response client.sendSms(request); if (!OK.equals(response.getBody().getCode())) { throw new RuntimeException(response.getBody().getMessage()); } return response; } }这段代码里最需要注意初始化阶段。Config对象负责承载AccessKey和EndpointEndpoint必须填短信服务网关所在的区域地址国内短信一般填dysmsapi.aliyuncs.com。如果把Endpoint填成其他产品的Endpoint请求会直接鉴权失败。另一个容易忽略的点是response.getBody()可能为空虽然正常情况下不出现但SDK调用失败时Body为空会导致空指针。发送接口的四个参数要分别说清楚。phoneNumbers是接收号码一次建议只传一个群发时用后面会说到的Batch接口避免一条短信里拼多个号码后不好定位问题。signName是控制台审核通过的签名名称不是签名ID。templateCode是模板CODE通常形如SMS_123456。templateParam是模板变量对应的JSON字符串模板里定义几个变量这里就传几个字段多传少传都会报错。在Controller层再包一层接口便于本地联调RestController RequestMapping(/sms) public class SmsController { private final SmsService smsService; public SmsController(SmsService smsService) { this.smsService smsService; } PostMapping(/send-code) public SendSmsResponse sendCode(RequestParam String phone, RequestParam String code) throws Exception { String json new ObjectMapper().writeValueAsString(Map.of(code, code)); return smsService.send(phone, json); } }Controller里直接Map.of生成模板参数Map里的key就是模板变量名。模板变量名区分大小写模板里写${code}这里就必须叫code。如果模板里定义了多个变量Map.of最多支持十组k-v足够日常场景。再复杂就改用LinkedHashMap保证顺序稳定。到这里最小工程已经能跑了。启动Spring Boot项目后用一个真实手机号调一次/sms/send-code能收到验证码就说明链路通了一半。接下来的问题是为什么有的签名发得出去、有的发不出去以及发送结果怎么追踪。这些放到下一章讲。3. 签名、模板与回执让短信真正到达用户手机3.1 签名和模板是“双前置条件”不是发完再补阿里云短信的发送链路要求签名和模板都必须审核通过。签名代表短信发送人显示在短信开头比如“【我的应用】”模板代表短信正文比如“您的验证码为${code}5分钟内有效”。这两样不是API申请完就能用需要先过审核。审核不通过时签名显示为“审核中”或“未通过”模板则无法用于发送。签名审核对新手来说是最容易卡住的地方。个人账号能申请的签名类型有限一般需要提供App应用商店截图、公众号主体信息或网站备案信息。如果之前没做过建议先到控制台看当前账号支持哪些签名来源再准备对应的资料。有一个常见误解是签名审核通过之后就一劳永逸实际上如果内容涉及敏感词或者用户投诉签名会被重新复核发送时就可能从“正常”变成“驳回”接口不会主动提示需要查询签名状态。模板审核则更看重“完整度”。模板不能是空泛的营销文案比如“恭喜您获得大奖”这类容易触发拦截。模板变量不要包住签名内容也不要把太多可变化的内容设计成变量。例如“${code}”把有效期固定为“5分钟”写进正文通过率更高。模板变量用${}包起来即可不需要自己定义完整JSON审核只看变量占位符和示例模板。3.2 TemplateParam参数模板变量必须严格匹配发送接口里TemplateParam是出错率最高的参数。很多人报isv.TEMPLATE_PARAMS_ILLEGAL想都不想就去改签名其实问题出在模板变量没对上。下面用例子说明模板正文是“您的验证码为${code}5分钟内有效”则TemplateParam只能传一个code字段例如{code:839204}。常见错误有两种。第一种是变量名不匹配模板里是${code}代码里却写{verifyCode:839204}。第二种是漏传模板里有${code}和${product}两个变量代码里只传了code。还有一种是类型问题模板变量值必须是字符串虽然某些SDK版本的数字也能通过但控制台侧的回执往往呈现为模板不合法。使用Jackson序列化可以避免手拼JSON时转义出错。如果手拼万一验证码本身包含引号或反斜杠非法字符会直接发送失败。用Map.of加ObjectMapper序列化以后值会被安全转义。注意模板变量值里不要包含换行和特殊符号运营商对这类内容很敏感。3.3 用QuerySendDetails确认发送状态别只看“OK”SendSms响应返回OK只代表请求被短信网关接收不代表用户手机百分之百收到。实际过程中短信可能被运营商拦截、被手机安全软件拦截、甚至因为号码处于黑名单而静默失败。要追踪最终状态需要查发送详情。最简单的查询方式是使用QuerySendDetails接口按手机号和日期查。下面这段代码展示了按天查找发送详情的逻辑public ListString querySendDetail(String phone, String date) throws Exception { QuerySendDetailsRequest request new QuerySendDetailsRequest() .setPhoneNumber(phone) .setSendDate(date) .setPageSize(10L) .setCurrentPage(1L); QuerySendDetailsResponse response client.querySendDetails(request); QuerySendDetailsResponseBody body response.getBody(); return body.getSmsSendDetailDTOs().stream() .map(detail - String.format(%s|%s, detail.getSendStatus(), detail.getErrCode())) .collect(Collectors.toList()); }这段代码有几个地方需要留意。sendDate格式必须是“年月日”的数字串比如20240101不能带横杠这是阿里云OpenAPI的固定格式。pageSize最大可以调大但单页返回的裸数据有限量很大时建议翻页查询。sendStatus和errCode是排查时最关键的字段sendStatus为“SEND_SUCCESS”才表示运营商接收成功为“SEND_FAIL”时就要看errCode里的运营商原因码。回执类字段在不同SDK版本里命名可能不同老版本SDK可能叫smsSendDetailDTOs新版本可能带嵌套结构。最好的做法是先把响应打印成JSON看一次实际结构再写映射逻辑不要直接照抄别人GitHub代码。这个习惯能避免大量“字段不存在”的编译错误也能帮你看明白回执里具体有哪些维度。用QuerySendDetails查单笔订单适合人工排查。如果每天短信量很大更建议接入阿里云短信回执消息服务通过队列异步接收状态报告。这个方案配置稍重但对账单核对和失败重推很有用后面讲生产化时再展开。4. 阿里云短信接口避坑清单5个“发不出去”的排查路径4.1 返回成功但用户收不到先查回执而不是反复重发现象接口调用返回OK控制台也显示发送成功但用户就是收不到短信。原因短信链路里“网关接收”和“用户收到”之间隔着运营商。可能被运营商拦截可能号码在退订黑名单也可能是模板内容触发手机安全软件拦截。只盯着API返回值看不到这些环节。解决用QuerySendDetails查该号码当天的发送详情。如果状态是“SEND_SUCCESS”再等几分钟看有没有失败回执推送确认到底卡在哪家运营商。这里我吃过亏某个测试号码被用户投诉过进入黑名单所有短信都静默失败业务侧一直提示发送成功用户却收不到。后台一查失败原因是“号码在黑名单”联系客服处理后才恢复。从此我坚持把发送状态落库每次发送都把BizId和手机号关联保存后续排查才有据可查。4.2 isv.SMS_SIGNATURE_ILLEGAL签名名称不是“看起来对”就行现象发送报isv.SMS_SIGNATURE_ILLEGAL但控制台里明明能看到签名。原因最常见是SignName参数和控制台不一致比如多了空格、大小写不同、用了签名备注而不是签名名称。另一种可能是签名审核通过后因投诉被暂停。解决先到控制台短信服务-签名管理里复制完整签名名称再回代码里比对。另外确认当前代码用的AccessKey所属账号和控制台登录账号是同一个。很多团队会有测试账号、生产账号两套账号代码里写的是测试账号的Key模板却审核在生产账号下两个账号里都有签名发送时就会因为签名归属不一致而报非法。以前我排查这个问题时折腾了两个小时才想到查账号归属真是一段血泪经验。4.3 isv.TEMPLATE_PARAMS_ILLEGAL手拼JSON翻车现象模板审核没问题签名也正确发送时却报模板参数不合法。原因TemplateParam里变量名写错、变量漏传或者JSON转义出了问题。尤其当验证码或链接中包含“”“”“”等字符手拼JSON时很容易多一个引号或少一个反斜杠。解决不用手拼用ObjectMapper的writeValueAsString序列化Map。同时把整个请求体打印到日志里和模板定义字段比对。打印日志时记得对手机号做脱敏不要把完整手机号和验证码拼在一条日志里。4.4 isv.BUSINESS_LIMIT_CONTROL触发了频率和总量限制现象短信接口平时正常一到活动或大促就大面积报业务限流。原因阿里云短信对单个手机号、单条模板、单个账号都有发送频率限制。比如同一个号码一分钟一次、一小时五次、一天十次账号级的总量超过阈值也会触发限流。活动场景下大量验证码集中发出很容易撞到阈值。解决先到控制台查看当前账号的配额和使用情况确认是单号被限还是总量被限。设计层面要做分级发送同一号码短时间内的验证码请求直接返回旧的验证码不重复调用短信接口高频异常请求直接拦截不走短信链路。如果确定业务合规可以提交工单提高阈值但阿里云给阈值设了上限不能无限调。4.5 测试环境能发生产环境发不出去RAM权限和账号串了现象本地测试是通的部署到生产后同一个代码报权限异常或者返回AccessDenied。原因生产环境的AccessKey没有授予短信相关权限或者生产环境配置里写了测试环境的Key测试账号和生产账号的资源和权限完全不同。还有可能是生产环境请求的Endpoint和本地不同虽然国内短信Endpoint一般固定但某些定制环境需要注意。解决优先检查配置来源是否从环境变量加载确认没有把配置文件里的占位符遗漏。然后用RAM控制台做权限模拟输入生产账号和Action名看是否允许。我现在的习惯是每套环境单独建一组AccessKeyKey名字带环境后缀代码里用环境变量注入从根上避免串号。排查权限问题时看返回码比翻日志更直接如果返回Code是NoPermission或AccessKeyId.NotFound第一反应就是查RAM策略而不是查模板。5. 从单发到批量群发限流预估、重试与幂等设计5.1 上线前先算峰值单号频率和全局QPS都要留余量很多人把短信模块写完就上线结果活动刚开始就被限流。短信和普通接口不一样背后是运营商通道阿里云产品层面有完整的风控体系。单条模板一般有账号级QPS限制具体数值在控制台“发送总量和频率限制”里能看到新账号往往比较低。我在设计发送模块时会先按业务峰值算两个数一是每秒最大请求数二是同一个手机号的最高发送频率。如果峰值QPS超出控制台阈值就必须做削峰。常见做法是用消息队列把发送请求异步化前端提交验证码请求后立即返回任务ID后端Worker按固定速率从队列拉取再调用短信接口。这样即使瞬时流量很大短信网关也不会被打爆。同时还要算“短信成本”。每发一条短信都要计费验证码短信虽便宜但一个用户误触多次、重复请求多次都会放大成本。比较好的做法是前端限制60秒内只能重发一次后端再做一次判断同一个手机号发送间隔小于60秒直接返回“发送频繁”这比盲目重发短信要省得多。5.2 幂等与重试业务ID和验证码有效期是关键短信接口天然适合做幂等用户点击一次获取验证码网络抖动后重试业务层不能让验证码短信每次都重新发送否则会收到多条不同验证码。解决办法是引入发送任务ID。发送前先查这个任务有没有成功记录如果已发送直接返回已有结果如果发送失败才重试。下面用简化示例说明幂等控制逻辑public String sendCodeWithIdempotent(String phone, String requestId) { String cached smsRecordService.get(requestId); if (cached ! null) { return cached; } String code RandomStringUtils.randomNumeric(6); try { smsService.send(phone, code); smsRecordService.save(requestId, phone, code, SUCCESS); return code; } catch (Exception e) { smsRecordService.save(requestId, phone, null, FAIL); throw e; } }这里的requestId由调用方生成可以是UUID也可以是手机号加时间戳。smsRecordService负责把发送记录存到Redis或数据库。重试时先走幂等查询已经发过就直接返回避免重复发送。这种设计在短信、邮件等所有外部渠道里都通用。重试本身要控制节奏。阿里云错误码里有一部分是临时性的比如网络超时、网关抖动可直接重试有一部分是永久性的比如签名非法、模板非法重试多少次都一样。实现时可以根据错误码分类决定是否重试。Spring项目里可以用Retryable注解也可以用隔离的定时任务扫描发送失败记录每5分钟补偿一次最多重试3次。我一般建议发送结果落库后由后台任务统一补偿而不是调用线程同步sleep重试这样调用方不会阻塞太久。5.3 批量下发先用SendBatchSms再决定线程池如果需要一次给大量用户发通知有两种做法循环调用SendSms或者使用SendBatchSms批量接口。循环调用实现简单但很容易触发QPS限制。SendBatchSms接口可以一次传入最多1000个手机号对应的模板变量也以数组结构传入虽然批量接口也会有频率限制但整体吞吐远高于单发循环。有一种情况不用批量接口即用户收到的短信内容完全不同比如每个用户的验证码或订单号不同。这时候SendBatchSms也能处理因为模板参数本身就是数组每个号码配一份JSON。不过要注意批量接口失败时定位是哪一条号码失败会比较麻烦响应的BizId对应整批次之后要用QuerySendDetails逐号码查。我通常的做法是业务量小于100条时直接用循环单发并为每个发送记录保存BizId大于100条时用SendBatchSms并保留原始号码列表用于后续对账。批量场景还要注意线程池配置。用线程池并发发送时线程数不要拍脑袋选20个最好按控制台配置的QPS和单次接口耗时反推。假设QPS限制是100一次SendSms耗时200ms那么并发线程数最多20个。线程数超过这个数只会增加排队和超时不会提升吞吐。6. 上线后的验证技巧用日志、拨测和消息ID把发送链路变成白盒短信模块上线后最大的风险不是代码逻辑而是“发不出去没人知道”。我后来养成一个习惯把短信发送做成一个可拨测的服务通过定时任务调用一次自己的发送逻辑输出成功或失败指标。如果连续几次拨测失败就触发告警。负责运维的同学不用登录控制台只看监控面板就知道短信链路是否正常。拨测和真实业务有什么不同拨测用的手机号是固定测试号发送内容写死不走业务模板变量。这样测的是“链路通不通”不会打扰真实用户。拨测频率不宜过高每5分钟一次就够了发送内容记得固定成不会被运营商拦截的简单文案比如“这是一条链路拨测短信”。拨测响应和真实发送一样也要把BizId记录下来连续失败时用QuerySendDetails查一下具体原因能区分是网关问题还是通道问题。另一个重要技巧是日志里强制带上消息ID。每一条发送日志都包含requestId、脱敏后的手机号、signName、templateCode、BizId、错误码。运维一旦收到用户反馈可以按手机号在日志平台里搜索一分钟还原这次发送全过程。如果项目已经做了AI Agent开发可以把这条查询能力封装成一个Tool让运维助手自动调用QuerySendDetails排查把人工盯屏变成机器自查。最后讲讲我的个人习惯。每次修改短信相关代码后先用自己的真实手机号发一条实际短信确认收到后再在日志里核对BizId这条流程已经成了肌肉记忆。短信这种依赖外部通道的功能最怕“我看后台成功了啊”式的盲目自信。把状态报告接进来、把拨测跑起来卡点就会浮出水面。希望帮到你。本文还有配套的精品资源点击获取