简介:面向PHP开发者的阿里云短信接口演示包,专用于验证码、通知及营销消息的快速接入,适合需要为网站或业务系统增加短信能力的初中级开发者。压缩包大小三点三五兆,内含阿里云官方PHP开发工具包核心库以及可直接运行的示例脚本,无需额外复杂配置即可在本地环境启动调试。目前已有二百七十四人学习下载。示例代码清晰展示了从初始化客户端、配置密钥、创建短信模板到调用发送接口完成短信发送的全过程;同时涵盖同步与异步两种调用模式、模板参数的动态替换、签名审核注意事项,以及常见异常如鉴权失败、发送频率超限时的错误处理与重试策略。还特别强调了加密传输与密钥安全管理等实践要点,有助于开发者在实际项目中规避安全隐患,并快速定位线上问题。
1. 某云短信接口 Demo:一条验证码从申请到送达的全链路落地
半夜上线最怕什么?不是代码崩了,而是验证码发不出去。我当时第一次接短信接口就是这种情况:签名以为审过了、模板也以为没问题,结果短信一直卡在“发送成功但用户收不到”的玄学状态。这份《某云短信接口 demo》压缩包,就是把“申请签名 → 申请模板 → 拼参数 → 发请求 → 看返回码”整条链路做成能直接跑的示例工程。它适合两类人:一是后端刚接触短信服务、想照着一套合格代码跑通联调的新手,二是要把短信能力快速嵌进自己项目的开发者——你可以直接抄里面的签名算法、请求封装和异常处理,省掉翻厂商文档的时间。
2. 项目结构与接入准备:从解压到第一条测试短信跑通
2.1 解压后目录长什么样
拿到压缩包先别急着双击运行,我习惯先看一遍目录结构,确认这项目是“能直接跑的最小工程”还是“需要拼装的散件”。这个 demo 属于前者,解压后的结构非常干净:
sms-demo/ ├── pom.xml # Maven 依赖与构建配置 ├── src/main/java/com/sms/demo/ │ ├── SmsApplication.java # Spring Boot 启动类 │ ├── config/ │ │ └── SmsProperties.java # 读取短信相关的自定义配置项 │ ├── controller/ │ │ └── SmsController.java # HTTP 接口入口,暴露发送/查询接口 │ ├── service/ │ │ ├── SmsService.java # 业务接口定义 │ │ └── impl/ │ │ └── SmsServiceImpl.java # 发送短信的核心实现 │ └── util/ │ └── SmsSignUtil.java # 请求参数签名工具类 └── src/main/resources/ ├── application.yml # 配置文件,密钥、签名、模板都在这里 └── logback-spring.xml # 日志输出配置一眼看过去就是标准的 Spring Boot 分层结构:controller 接收请求,service 处理业务,util 放签名算法。config 那个包很关键,它把云厂商的密钥、签名、模板 ID 这些参数从代码里抽出来了,改配置不用改代码,这点对后面二次开发很重要。
pom.xml 里依赖并不多,核心就两个:Spring Boot Web 用来提供 HTTP 接口,加上云厂商官方短信 SDK 的 Java 包。如果你拿到的版本没有 SDK 依赖,也可以直接用 Hutool 或 Apache HttpClient 发 POST 请求,但用官方 SDK 的好处是签名算法和请求封装都替你做好了,踩坑概率小很多。
2.2 开通短信服务:签名与模板的申请顺序不能乱
这个 demo 能跑通的前提是你在云厂商控制台把前置条件准备好。很多人第一次接短信就翻车在顺序上:先去申请模板,结果被告知“必须先有签名”。正确的流程是这样:
第一步,开通短信服务。在云厂商控制台找到短信服务,按实名认证要求完成开通,这一步一般几分钟生效。开通之后你才能看到 AccessKey 的创建入口。
第二步,申请短信签名。签名就是短信开头那截【】里的内容,比如【某公司】。申请时需要提供使用场景说明,比如“用于用户登录验证码”,并上传对应的资质材料。这里有个血泪经验:签名内容要和你的业务强相关,别起一个和自己网站完全无关的名字,审核大概率被打回。
第三步,申请短信模板。模板是短信正文的骨架,比如“您的验证码为${code},请在${minutes}分钟内填写”。变量要用${}占位,不能写死。变量名要语义化,审核员要能看懂这个变量是干什么的。我见过有人把验证码变量写成${a},被打回的理由是“变量含义不明”。
等签名和模板都审核通过后,控制台会给你两个标识:签名名称和模板编码(模板 ID)。这两个值加上 AccessKey,就是后面配置文件里要填的全部内容。
2.3 配置文件落位:密钥、签名、模板编号怎么填
拿到审核通过的签名和模板之后,打开 demo 里的 application.yml,把参数填进去:
sms: access-key-id: LTAI5tXXXXXXXXXXXXXX access-key-secret: 你的AccessKeySecret sign-name: 某公司 template-code: SMS_123456789 region-id: cn-hangzhou endpoint: example.com这里有几个容易填错的地方。access-key-secret 是创建 AccessKey 时系统只显示一次的那个长串,如果不小心关掉了页面就得删掉重建。sign-name 要填签名内容本身,不带中括号,比如你申请的签名显示为【某公司】,配置里填“某公司”,不要把【】也带上。
template-code 是模板 ID,就是 SMS_ 开头的那串数字,不是模板内容。region-id 一般是 cn-hangzhou,除非你的业务在别的区域,否则不用动。endpoint 默认用厂商官方接入地址即可,demo 里已经内置了,一般不需要改。
填完之后,启动 Spring Boot 工程,浏览器直接访问:
GET http://localhost:8080/sms/send?phone=13800000000&code=123456如果日志里出现“发送成功”且返回码为 OK,第一条测试短信就算跑通了。收不到短信的,先去检查手机号是不是测试专用号,以及前面三步申请有没有都通过。
3. 核心链路拆解:从参数拼装到 HTTP 返回的完整流程
3.1 先认清账号体系三件套
这个 demo 能跑起来,本质上靠的是三样东西对齐:AccessKey 标识“你是谁”,签名标识“你代表谁发”,模板标识“你要发什么”。三者缺一不可,而且它们之间是绑定的。签名属于某个账号,模板也属于某个账号,如果你改了 AccessKey,原来的签名和模板就都失效了。
有个常见误会是“我有了 AccessKey 就能直接调接口”。其实不行,签名和模板的审核状态决定你能不能发正式短信。AccessKey 只是通行证,签名和模板是内容合规的准入证明。demo 的 SmsProperties 里把这三样都定义成配置项,也是因为它们在真实项目里通常是可变的——测试环境一套签名模板,生产环境一套,代码不用动,改配置就行。
3.2 签名算法:参数如何参与加密
虽然用官方 SDK 会自动做签名,但理解签名算法仍然重要。因为一旦要排查“为什么报签名不匹配”,不懂算法就只能瞎试。这个 demo 里的 SmsSignUtil 承担了签名计算的工作,核心逻辑是这样的:
public class SmsSignUtil { // 对请求参数按字典序排序后做 HMAC-SHA1 签名 public static String sign(Map<String, String> params, String secret) throws Exception { // 1. 过滤掉 value 为空的参数 Map<String, String> sortedParams = new TreeMap<>(); for (Map.Entry<String, String> entry : params.entrySet()) { if (entry.getValue() != null && !entry.getValue().isEmpty()) { sortedParams.put(entry.getKey(), entry.getValue()); } } // 2. 拼接成 key1=value1&key2=value2 的待签名字符串 StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> entry : sortedParams.entrySet()) { sb.append(entry.getKey()) .append("=") .append(URLEncoder.encode(entry.getValue(), "UTF-8")) .append("&"); } String queryString = sb.substring(0, sb.length() - 1); // 3. 用 AccessKeySecret 作为密钥做 HMAC-SHA1,再 Base64 编码 Mac mac = Mac.getInstance("HmacSHA1"); mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA1")); byte[] signBytes = mac.doFinal(queryString.getBytes("UTF-8")); return new String(Base64.getEncoder().encode(signBytes), "UTF-8"); } }逻辑说明:第一步用 TreeMap 排序,保证参数顺序稳定,这是签名校验的第一道规矩——服务端会用同样的排序规则计算签名,顺序不一致结果必不同。第二步做 URL 编码,中文和特殊字符必须转成 UTF-8 百分号编码,像“某公司”里的中文如果不编码,签名会直接算错。第三步用 AccessKeySecret 当 HMAC 密钥,注意不是 AccessKey ID,很多第一次用的人把这两个搞混。
参数说明:params 是请求的全部业务参数,包括手机号、模板编码、签名名称等;secret 是对应的 AccessKeySecret。如果服务器返回“SignatureDoesNotMatch”,优先怀疑两点:一是参数里有中文没编码,二是自己拼的参数名和服务端要求的不一致。
3.3 发送验证码:客户端调用与请求构造
签名工具类理解了,再看发送方法的实现。demo 里 SmsServiceImpl 的 sendCode 方法封装了从参数组装到发送的完整过程:
@Service public class SmsServiceImpl implements SmsService { @Autowired private SmsProperties smsProperties; @Override public SmsSendResponse sendCode(String phone, String code) { // 1. 组装请求参数,模板里的 ${code} 由这里传入 Map<String, String> params = new HashMap<>(); params.put("PhoneNumbers", phone); params.put("SignName", smsProperties.getSignName()); params.put("TemplateCode", smsProperties.getTemplateCode()); params.put("TemplateParam", "{\"code\":\"" + code + "\"}"); // 2. 调用云厂商 SDK 的发送接口 SmsClient client = new SmsClient( smsProperties.getAccessKeyId(), smsProperties.getAccessKeySecret(), smsProperties.getEndpoint() ); SendSmsRequest request = new SendSmsRequest(); request.setPhoneNumbers(phone); request.setSignName(smsProperties.getSignName()); request.setTemplateCode(smsProperties.getTemplateCode()); request.setTemplateParam("{\"code\":\"" + code + "\"}"); SendSmsResponse response = client.sendSms(request); // 3. 把响应转成统一的返回对象 SmsSendResponse result = new SmsSendResponse(); result.setCode(response.getCode()); result.setMessage(response.getMessage()); result.setBizId(response.getBizId()); return result; } }逻辑说明:第一部分是参数组装,TemplateParam 是 JSON 字符串,要和模板里的变量名严格对应。模板里写的是 ${code},这里 JSON 的 key 就必须是 code,写成 Code 或验证码都会导致模板变量填充失败。第二部分是 SDK 调用,SmsClient 的构造函数吃三个参数,顺序是 AccessKey ID、AccessKey Secret、Endpoint,别传反。第三部分把 SDK 的响应转成自己的返回对象,方便上层统一处理。
参数说明:phone 是要接收短信的手机号,国内号直接填 11 位即可;code 是你生成的验证码。这里有个细节值得学习:demo 没有把 SDK 的响应对象直接抛给 controller,而是包了一层自己的 SmsSendResponse,这样就算以后换云厂商,上层业务代码也不用改,只要实现层做适配。
3.4 返回码不是 OK 时怎么排查
短信接口的返回结构里,Code 字段是核心判断依据。demo 里也做了简要处理:Code 为 OK 时走成功流程,否则进入异常分支。我挑几个高频失败码说一下排查思路。
“isv.SMS_SIGNATURE_ILLEGAL”表示签名不合法。先确认配置里的签名名称和审核通过的是否一字不差,再看签名是否已经被处罚或过期。这个坑很隐蔽,签名在控制台可能显示正常,但实际状态是“停用”,这时候唯一的解决方式是提交申诉或重新申请。
“isv.MOBILE_NUMBER_ILLEGAL”是手机号格式不对。检查是不是传了带 86 前缀的号码,或者混入了空格。如果测试环境循环发了几百条,还会触发“isv.BUSINESS_LIMIT_CONTROL”,这是触发了流控限制——同一号码每小时最多发 5 条验证码,每天最多 10 条。这不是 bug,是厂商的安全策略,代码层面无法绕过,只能在业务上提示用户“操作过于频繁”。
还有一种情况让人很崩溃:返回 OK,但手机收不到短信。这不是接口的问题,而是内容被运营商拦截了。常见原因是模板里含“测试”两个字,很多运营商对测试短信直接落拦截。这个坑下一章详细说。
4. 避坑指南:短信接口开发里常见的五个翻车现场
4.1 签名审核通过,但模板没审核,发送照样失败
现象:签名已经在控制台显示“已生效”,调用接口却返回“模板未备案”或“TemplateCode 不存在”。
原因:签名和模板是两套独立审核流程。签名过了不代表模板能用。你申请的模板可能还在审核中,或者模板内容与实际调用参数不匹配。
解决:登录控制台确认模板状态为“已通过”。如果模板已通过但接口还报错,大概率是配置里的模板 ID 填错了,复制粘贴时容易带上多余空格。我当时就因为这个空格排查了半小时。另外,模板审核通过后有个生效延迟,通常几分钟,刚通过时调接口有时会报“模板不存在”,等一会再重试即可。
4.2 模板变量名对不上,验证码变成空值
现象:短信收到的是“您的验证码为,请在5分钟内填写”,验证码位置是空白。
原因:模板定义的是 ${code},但代码里 TemplateParam 传的是 {"var":"123456"},变量名和模板占位符不一致,系统不会报错,只是填充失败。
解决:严格对照模板内容里的变量名,一个字符都不能差。凡是模板里有两个变量,比如 ${code} 和 ${minutes},TemplateParam 就要同时传两个 key。我建议把模板变量名校验写进单元测试里,测试用例直接断言 TemplateParam 的 JSON 包含所有模板占位变量,避免上线后才发现。
4.3 AccessKey 泄露,被人刷了大量短信
现象:账单异常,短信发送量在凌晨突然暴涨,明明是测试环境。
原因:AccessKey ID 和 Secret 被提交到了 Git 仓库,或者写死在前端代码里。云厂商会扫描公开仓库,一旦发现 Key 泄露会自动禁用,但禁用之前可能已经被刷。
解决:立刻在控制台禁用并删除泄露的 AccessKey,新建一个。配置文件里的密钥必须用环境变量或配置中心注入,绝不能写进 application.yml 提交仓库。这个 demo 虽然把密钥放配置文件里,但那是为了本地演示,二开的时候第一件事就是把密钥挪到环境变量:
export SMS_ACCESS_KEY_ID=你的ID export SMS_ACCESS_KEY_SECRET=你的Secret4.4 同一手机号短时间重复发送,直接被限流
现象:用户点了几次“获取验证码”,第三次开始报“触发分钟级流控”。
原因:云厂商对同一号码的发送频率有硬限制:同一号码一分钟 1 条、一小时 5 条、一天 10 条。代码里没做前置拦截,请求直接打到厂商接口才发现被限流。
解决:在业务层加验证码发送间隔控制,比如 60 秒内不允许重复点击。demo 里没有这个逻辑,这是最常见的二次改造点。下一章我会给出完整的改造方案,用 Redis 做冷却计数,既省短信费又不被限流。
4.5 返回 OK 但用户收不到:测试模板被运营商拦截
现象:接口返回 Code=OK,手机收不到短信,但同一模板换个时间发送又偶尔能收到。
原因:模板内容触发了运营商拦截策略,尤其是包含“测试”“试用”这类关键词。有的云厂商有专门的白名单测试模板,但发送真实手机号时容易被拦截。
解决:不用测试模板发正式手机号。申请正式模板时,内容尽量贴近真实场景,比如“您的登录验证码为${code},5分钟内有效”,别写“这是一条测试短信”。另外,夜间发送营销类短信也容易进拦截,验证码类短信相对好一些,但凌晨 2 点到 4 点发送依然有延迟送达的情况。
5. 二次开发实战:把 demo 改造成带频率限制的验证码服务
5.1 抽象发送接口,把厂商 SDK 关进抽屉
demo 里的发送逻辑直接放在了 SmsServiceImpl 里,controller 调 service,service 调 SDK。这在 demo 阶段够用,但进了真实项目,我建议先做一层接口抽象,把 SDK 彻底隔离在 impl 内部,这样日后换厂商、加 mock、做单元测试都会轻松很多。
public interface SmsSender { /** * 发送验证码 * @param phone 手机号 * @param code 验证码 * @return 是否发送成功 */ SmsSendResult send(String phone, String code); }impl 里面做的事情和 demo 保持一致,但返回对象换成自己的 SmsSendResult,里面包含 code、message、requestId 三个字段。之所以保留 requestId,是因为排查问题时要拿这个 ID 去厂商后台查真实状态。如果哪天要换厂商,只需要重新写一个 impl,上层业务代码完全不用动。
5.2 验证码生成、缓存与 60 秒重发冷却
真实项目里验证码不能每次发送都重新生成就算完,至少要解决两个问题:一是防止用户在 60 秒内反复点击,二是验证码要有有效期。我用 Redis 做校验码存储,五分钟过期,同时用一个单独的 key 做冷却标记:
@Service public class VerificationCodeServiceImpl implements VerificationCodeService { @Autowired private SmsSender smsSender; @Autowired private StringRedisTemplate redisTemplate; // 验证码有效期 5 分钟 private static final long CODE_EXPIRE_SECONDS = 300; // 重发冷却 60 秒 private static final long RESEND_COOLDOWN_SECONDS = 60; // 验证码 key 前缀 private static final String CODE_KEY_PREFIX = "sms:code:"; @Override public boolean sendCode(String phone) { // 1. 检查冷却时间是否已到 String cooldownKey = "sms:cooldown:" + phone; Boolean canSend = redisTemplate.hasKey(cooldownKey); if (Boolean.TRUE.equals(canSend)) { // 还在冷却期,直接拒绝 return false; } // 2. 生成 6 位随机验证码 String code = String.valueOf((int) ((Math.random() * 9 + 1) * 100000)); // 3. 先存 Redis,再发短信;如果发送失败则删除缓存,允许用户重试 String codeKey = CODE_KEY_PREFIX + phone; redisTemplate.opsForValue().set(codeKey, code, CODE_EXPIRE_SECONDS, TimeUnit.SECONDS); redisTemplate.opsForValue().set(cooldownKey, "1", RESEND_COOLDOWN_SECONDS, TimeUnit.SECONDS); SmsSendResult result = smsSender.send(phone, code); if (!result.isSuccess()) { redisTemplate.delete(codeKey); redisTemplate.delete(cooldownKey); return false; } return true; } @Override public boolean validateCode(String phone, String code) { String codeKey = CODE_KEY_PREFIX + phone; String cachedCode = redisTemplate.opsForValue().get(codeKey); if (cachedCode != null && cachedCode.equals(code)) { // 校验成功立刻删除,防止重放 redisTemplate.delete(codeKey); return true; } return false; } }逻辑说明:发送之前先查冷却 key 是否存在,存在说明 60 秒内发过,直接拒绝。验证码用 6 位随机数,实际项目可以用 SecureRandom 更安全。先写 Redis 再调短信接口,好处是短信发送失败时可以把缓存删掉,恢复用户的重试权利,避免一次失败把用户的验证码资格也消耗掉。
参数说明:CODE_EXPIRE_SECONDS 是验证码有效期,一般 5 分钟是业界常见的平衡点;RESEND_COOLDOWN_SECONDS 是重发冷却,60 秒能挡住大部分手误操作。validateCode 里校验成功就删 key,这是防止验证码被重复使用的基本姿势。
5.3 状态查询与日志记录:让拦截和失败有迹可循
demo 只实现了发送,没有状态查询。真实环境里“发送成功”不等于“送达成功”,中间可能被运营商拦截或手机端过滤。云厂商提供了短信发送记录查询接口,可以根据 bizId 查某条短信的流转状态。改造时我在 service 里加了一个查询方法:
public SmsQueryResult querySendStatus(String phone, String bizId) { // 调用厂商查询接口,返回状态包括:等待回执、发送成功、发送失败 QuerySmsDetailRequest request = new QuerySmsDetailRequest(); request.setPhoneNumber(phone); request.setBizId(bizId); // 返回的 status 字段含义:1 等待回执,2 发送成功,3 发送失败 QuerySmsDetailResponse response = smsClient.querySmsDetail(request); return convertResponse(response); }这个查询一般只支持当天数据,超过 24 小时的历史记录要去控制台导出。我的建议是:发送短信的同时把 phone、bizId、templateCode、发送时间写进业务日志,每次用户反馈“没收到”时有据可查。日志字段可以参考下面这张表:
| 字段 | 类型 | 说明 |
|---|---|---|
| phone | string | 接收手机号 |
| bizId | string | 厂商返回的流水号,排查依据 |
| templateCode | string | 实际使用的模板 ID |
| signName | string | 签名名称 |
| respCode | string | 接口返回码 |
| sendTime | datetime | 发送时间 |
| queryStatus | int | 状态查询结果:1 等待回执 / 2 成功 / 3 失败 |
日志别只打“发送失败”,要把 respCode 一起打出来。我踩过这个坑:日志只记录成功失败,用户说没收到短信,我连失败原因都不知道,还得翻厂商控制台。
6. 从 demo 到生产环境:参数隔离、重试与日志落库的收尾改法
demo 的价值在于跑通链路,生产环境的短信服务要在三个方向上做加固:参数环境隔离、发送重试、日志落库。
第一,参数隔离。开发环境用测试签名和测试模板,生产环境用正式签名模板,两者绝不能混。我的做法是把密钥和签名模板配置从 application.yml 挪走,改用环境变量注入,Spring Boot 里直接用${SMS_SIGN_NAME}占位符读取。这样 Git 仓库里永远不会出现真实密钥,开发上线时也不用改代码,只改环境变量。
第二,重试策略。短信发送失败分两类:一类是业务错误,比如模板不匹配、签名非法,这种重试一万次也没用,直接告警;另一类是网络超时、限流等临时性错误,这种可以做有限重试。我用 Spring Retry 做了一个最多重试 2 次的重试模板,每次间隔 500 毫秒,并只在网络异常和限流错误时触发。注意重试要做好幂等防护——同一个手机号不要因为重试就发两条短信,重试前先查 Redis 里是否已经有这轮的发送记录。
第三,日志落库。Redis 里的验证码过期就没了,但业务上可能需要追溯“用户到底是什么时间向哪个手机号发了验证码”。我在改造时加了一张短信发送流水表,核心字段是 id、phone、biz_id、template_code、resp_code、status、create_time。status 字段用 smallint:0 是发送中,1 是成功,2 是失败。配合定时任务每小时拉一次云厂商的状态查询接口,把最终回执状态回填到表里。
这里有一个我要强调的验证方法:所有改造完成后,不要只在测试环境跑一遍就觉得万事大吉。我在一次上线时把签名的配置项 Key 名写错了,代码启动后一直没有报错,直到用户反馈收不到验证码才查出来。从那以后,我每次上线前都强制走一遍“试发流程”:用自己的手机号发一条真实短信,确认收到后再放量。这个过程只需要一分钟,但能拦住大部分低级配置错误。希望帮到你。
本文还有配套的精品资源,点击获取