☰
SpringBoot 接入华为云短信服务三步实操:从鉴权到业务联调
2026/10/11 13:15:20 网站建设 项目流程

做后端这几年,短信验证码算是接得最多的第三方服务之一,从账号注册、找回密码到登录二次校验,短信通道一旦出问题,整个业务流程都得跟着停摆。这次项目里需要把短信能力接到华为云短信服务上,网上搜了一圈,资料要么停留在老版控制台截图,要么只讲文档里已经废弃的参数,能照着跑通的不多。这篇内容就是我实际接入过程的完整记录,按“三步接入”的思路来组织——准备阶段、工程集成、业务联调,每一步的选型理由、核心代码和踩过的坑都会讲清楚。如果你正打算在 SpringBoot 里接华为云短信服务,或者已经被鉴权那段绕得头晕,这篇应该能帮你省掉不少试错时间。

1. 为什么是“三步”:接入前的整体设计

写代码之前,先把思路捋清楚。短信接入链路看着不长,实际牵扯到账号资质、签名审核、模板审核、鉴权算法、回调处理好几个环节,任何一个出问题都会把整个流程卡住。我把接入过程压缩成三步,不是少了步骤,而是把最容易绕晕的部分归类到三个阶段,每个阶段都有明确产出,验收起来也方便。

1.1 华为云短信服务解决了什么问题

短信发送这件事,表面上是“给用户发一条消息”,实际背后是运营商通道、签名报备、模板审核、状态回执、并发处理这一整套链路。自建通道既不现实也不合规,云厂商把这些底层事都打包好了,我们只需要把业务参数填进去,让平台把短信发出去就行。

我选华为云短信服务,主要看中几点:

  • 国内通道覆盖三网,实名认证之后就能申请使用。
  • 签名和模板支持在线申请,审核进度透明可控。
  • 提供状态回调接口,能精确记录每条短信的送达结果。
  • 鉴权方式稳定,适合封装成独立模块在工程里复用。

说句实话,各家短信服务商提供的功能大差不差,真正影响开发体验的是接入文档的清晰度和审核效率。华为云的控制台和文档更新得挺勤快,但正因为更新快,网上很多老教程的截图已经对不上了。我写这篇时采用的接口形态和参数规则,尽量贴近当前版本。如果你照着操作时发现控制台界面和我描述的不完全一致,大概率是功能位置挪了,搜索对应关键词就能找到。

1.2 三步法的拆解逻辑

我的三个步骤划分是这样的:

第一步,准备资源和资质。包括账号实名认证、创建短信应用、申请签名和模板,拿到 App Key 和 App Secret。这一步的产出是“所有一次性资源都就绪,后面写代码时不会因为缺东少西而中断”。

第二步,工程集成。在 SpringBoot 工程里配置连接参数、实现鉴权工具类、封装发送短信的方法。这一步的产出是“代码能成功发出一短信”。

第三步,业务联调。把发送能力接入真实场景,比如登录验证码,配合 Redis 做防刷限流、存储验证码、接收状态回调。这一步的产出是“业务闭环完全跑通”。

这样拆最大的好处是每步的验收标准非常清楚。我见过不少同事一上来就写发送代码,写完之后才发现签名还没申请、模板没过审、密钥找不到了,回头再补齐这些准备工作,等于全部返工。把准备阶段单独拎出来,强制优先级,能绕开这些坑。

1.3 三个容易混淆的核心概念:应用、签名、模板

新手最容易搞混的是应用、签名、模板三者的关系。

用生活化的比喻:应用是你的“业务入口”,相当于寄快递时选的那个网点;签名是包裹上显示的寄件人抬头,用户看到的“【某科技】”就是它;模板是发货清单,规定了短信的正文长什么样、哪些位置可以填变量。

具体到华为云控制台:

  • 短信应用:创建后生成 App Key 和 App Secret,这两个值用于后续 API 鉴权。
  • 短信签名:申请时需要关联某个应用,审核通过后才能在发送接口中使用。
  • 短信模板:同样关联应用,模板里的变量用${1}、${2}占位。

发送短信时,接口请求里同时携带签名内容和模板 ID,服务端会根据“应用 + 签名 + 模板”的组合关系做校验。这也解释了一个常见现象:签名审核不通过时,模板就算审核过了也发不出短信,因为签名和应用没绑定成功。所以准备阶段的核心任务,就是把这三个资源全部备齐。

2. 第一步:准备资源与资质

这一步主要在控制台操作,不写代码,但它的优先级最高。我按实际操作顺序写一遍,每个环节的注意事项都标出来。

2.1 账号实名认证,别在这里省时间

注册华为云账号后,第一件事就是完成实名认证。个人认证能注册账号,也能进控制台,但短信签名申请对主体要求很严格,企业相关的签名类型基本都要求企业认证。

我的建议是:如果你是替公司业务接入,直接走企业认证流程,把营业执照准备好,按提示上传,一般几分钟到几小时就能通过。如果先做了个人认证再改企业认证,中间要额外提交材料,反而耽误时间。

注意:实名认证的主体要和签名内容匹配。比如签名想申请“某科技”(假设公司名包含“某”字),认证主体就得是这家公司,否则审核人员大概率会驳回。

2.2 创建短信应用,拿到密钥

登录华为云控制台,搜索“消息&短信服务”进入管理页面:

  1. 左侧菜单选择“短信应用”。
  2. 点击“创建应用”,填写应用名称,比如“登录验证码”或“通知发送”。
  3. 创建完成后进入应用详情,能看到 App Key。

重点是 App Secret。它只在创建成功时显示一次,错过之后只能通过“重置密钥”再获取一次。我踩过的坑是创建完随手截了个图,后来清理截图把密钥一起删了,只能重置,好在当时还没上线。正规做法是创建完立刻把 App Key 和 App Secret 记到团队的密钥管理工具里,不要在聊天工具里传来传去,更不要写死在代码里。

2.3 申请短信签名与模板

签名和模板都在控制台提交申请,审核周期通常 1 到 2 个工作日,有的加急当天能过。这一节是准备阶段里最需要细心的部分。

申请签名

需要填写的关键信息:

  • 签名内容:用户最终看到的发送方标识,比如“某科技”。
  • 签名类型:根据品牌载体选择,比如 APP 应用、网站、公众号。
  • 适用范围:写清楚业务场景,比如“用户登录时发送验证码”。

如果是 APP 应用签名,通常需要提供软件著作权证书或应用商店上架截图;网站签名则需要提供网站备案号。材料越齐全,一次性通过的概率越高。

申请模板

模板类型有验证码、通知、营销等。以验证码模板为例:

您的验证码为${1},${2}分钟内有效。如非本人操作,请忽略本短信。

变量规则有三个重点:

  • 变量用${数字}表示,数字是变量序号,从 1 开始。
  • 变量不能连续出现,比如${1}${2}这种写法不合法,中间至少要有一个固定字符。
  • 提交审核时必须写明每个变量的含义,比如“${1}:6位数字验证码”。

我在模板审核上踩过的坑:某次写了营销类模板,内容里带链接但没有退订文案,被驳回了。后来加上“回复TD退订”才通过。说白了,审核关心的不只是格式正确,还有合规和用户体验。所以申请信息里的“业务描述”别偷懒,写清楚用户会在什么场景收到短信,通过率会明显提高。

3. 第二步:SpringBoot 工程集成

现在进入写代码阶段。工程集成拆成三块:依赖、配置、工具类。这三块做好,短信发送能力就具备了。

3.1 依赖选择:只要 Spring Web

很多人的惯性思维是找官方 SDK,先加一坨依赖再说。我的选择是不加 SDK,直接基于 HTTP 接口实现。原因有两个。

第一,短信服务的调用链路非常轻,本质就是一个 POST 请求加一个鉴权头,RestTemplate 完全可以覆盖。第二,SDK 版本更新频繁,不同版本之间 API 有差异,网上教程经常对不上,反而增加排错成本。

工程里只需要 Spring Web 提供的依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>

如果你的项目不是 Web 工程,单独引入 spring-boot-starter-web 即可。短信发送用同步 REST 调用就够,没有必要上消息队列这类重组件。

3.2 配置参数:密钥不要裸奔

把短信相关配置放到application.yml:

sms: huawei: app-key: ${SMS_APP_KEY} app-secret: ${SMS_APP_SECRET} endpoint: https://msgsms.cn-north-4.myhuaweicloud.com:443 sender: "某科技" template-id: "your-template-id" callback-url: https://your-domain.com/sms/callback

各参数含义:

  • app-key、app-secret:创建短信应用后获得的鉴权凭证。
  • endpoint:短信服务接入地址,以控制台“应用接入”页面展示的地址为准,不同区域不一样。
  • sender:签名内容,注意是签名本身,不是签名 ID。
  • template-id:模板 ID,审核通过后可在控制台查到。
  • callback-url:状态回调地址,可选的,不填不影响发送,但建议配置。

密钥用${SMS_APP_KEY}占位符引用环境变量,不要在 git 仓库里提交真实密钥。这个习惯非常重要,我见过不止一次公司后台源码泄露,短信密钥跟着被滥用,一夜之间产生大量扣费短信。

然后定义配置属性类:

@Component @ConfigurationProperties(prefix = "sms.huawei") public class SmsProperties { private String appKey; private String appSecret; private String endpoint; private String sender; private String templateId; private String callbackUrl; // getter / setter 省略 }

3.3 核心工具类:几乎所有人都会卡在鉴权上

短信接口的关键在于 WSSE 鉴权。华为云短信 API 要求每个请求都必须携带Authorization和X-WSSE两个请求头。

先看完整代码,再解释算法:

@Component public class SmsUtil { private final SmsProperties properties; private final RestTemplate restTemplate; public SmsUtil(SmsProperties properties, RestTemplateBuilder builder) { this.properties = properties; this.restTemplate = builder .setConnectTimeout(Duration.ofSeconds(5)) .setReadTimeout(Duration.ofSeconds(10)) .build(); } public SendResult send(String to, String templateParas) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("Authorization", "WSSE realm=\"SMS\", profile=\"UsernameToken\", type=\"Appkey\""); headers.set("X-WSSE", buildWsseHeader( properties.getAppKey(), properties.getAppSecret())); Map<String, Object> body = new HashMap<>(); body.put("from", properties.getSender()); body.put("to", to); body.put("templateId", properties.getTemplateId()); body.put("templateParas", templateParas); HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(body, headers); String url = properties.getEndpoint() + "/sms/batch/send"; ResponseEntity<SmsApiResponse> response = restTemplate.postForEntity(url, requestEntity, SmsApiResponse.class); if (response.getStatusCode().is2xxSuccessful() && "000000".equals(response.getBody().getCode())) { return SendResult.success(response.getBody().getSmsId()); } return SendResult.failed(response.getBody().getDescription()); } private String buildWsseHeader(String appKey, String appSecret) { String nonce = UUID.randomUUID().toString().replace("-", ""); String created = DateTimeFormatter .ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'") .format(LocalDateTime.now(ZoneOffset.UTC)); byte[] passwordBytes = hmacSha256( appSecret.getBytes(StandardCharsets.UTF_8), (nonce + created).getBytes(StandardCharsets.UTF_8)); String password = Base64.getEncoder().encodeToString(passwordBytes); return String.format( "UsernameToken username=\"%s\", password=\"%s\", nonce=\"%s\", created=\"%s\"", appKey, password, nonce, created); } private byte[] hmacSha256(byte[] key, byte[] data) { try { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(key, "HmacSHA256")); return mac.doFinal(data); } catch (Exception e) { throw new SmsException("HMAC-SHA256 计算失败", e); } } }

代码里的SendResult、SmsApiResponse、SmsException是我自己封装的基础类。SendResult包含success、smsId、desc三个字段,SmsException继承 RuntimeException,你可以按自己项目的规范定义,这里不占篇幅贴全部代码了。

现在解释 WSSE 鉴权算法,这是很多人第一次接触时最容易懵的地方:

  1. 生成一个随机字符串nonce,每次请求都要不同,它是为了防止重放攻击。
  2. 取当前 UTC 时间,格式化成为created。
  3. 用 HmacSHA256 算法,以 App Secret 为密钥,对nonce + created的拼接串做签名,得到字节数组后再 Base64 编码。
  4. 把 App Key、编码后的 password、nonce、created 四个值按固定格式拼进X-WSSE请求头。

两个关键点:签名时 App Secret 是 HMAC 的 key,nonce + created是 data,顺序反了服务端会一直报鉴权失败。另外created必须是 UTC 时间,不是本地时间,如果直接用LocalDateTime.now()不带时区,请求会被当成过期或无效。

3.4 快速验证发送能力

写一个简单的测试方法,先验证工具类能不能通:

@SpringBootTest class SmsUtilTest { @Autowired private SmsUtil smsUtil; @Test void sendCode() { String params = "[\"123456\",\"5\"]"; SendResult result = smsUtil.send("13800138000", params); System.out.println(result); assertTrue(result.isSuccess()); } }

第一次发送不成功别慌,先看接口返回的code和description。常见的鉴权错误、模板参数错误都在第 5 章列出来了。如果你改了 HMAC 计算方法,建议先用文档里的示例参数手算一遍 Base64 输出,确认无误再放到工具类里。

4. 第三步:业务联调与验证

工具类能发短信了,接下来把它接进真实业务。这一步不只是“调一个接口”这么简单,要考虑防刷、验证码存储、回调处理、发送记录留痕。

4.1 验证码发送的完整链路

以最常见的“手机号 + 验证码登录”为例,完整流程是这样的:

  1. 用户提交手机号,点击“获取验证码”。
  2. 后端检查该手机号在最近 60 秒内是否已经发过(防刷)。
  3. 生成 6 位随机码,存入 Redis,设置 5 分钟过期。
  4. 调用 SmsUtil 发送短信。
  5. 前端收到“发送成功”,开始倒计时。
  6. 用户提交验证码,后端从 Redis 取出比对。通过后标记该手机号已验证。

代码实现:

@Service public class AuthService { @Resource private SmsUtil smsUtil; @Resource private StringRedisTemplate redisTemplate; private static final String CODE_PREFIX = "sms:code:"; private static final String FLOOD_PREFIX = "sms:flood:"; private static final long CODE_TTL = 5; private static final long FLOOD_TTL = 60; public void sendCode(String phone) { Boolean first = redisTemplate.opsForValue() .setIfAbsent(FLOOD_PREFIX + phone, "1", FLOOD_TTL, TimeUnit.SECONDS); if (!Boolean.TRUE.equals(first)) { throw new BizException("发送太频繁,请稍后再试"); } String code = String.format("%06d", ThreadLocalRandom.current().nextInt(1000000)); String params = "[\"" + code + "\",\"" + CODE_TTL + "\"]"; SendResult result = smsUtil.send(phone, params); if (!result.isSuccess()) { throw new BizException("短信发送失败:" + result.getDesc()); } redisTemplate.opsForValue().set( CODE_PREFIX + phone, code, CODE_TTL, TimeUnit.MINUTES); } public boolean verifyCode(String phone, String inputCode) { String cached = redisTemplate.opsForValue().get(CODE_PREFIX + phone); if (cached != null && cached.equals(inputCode)) { redisTemplate.delete(CODE_PREFIX + phone); return true; } return false; } }

两个细节值得说。生成验证码用ThreadLocalRandom,比Math.random()在并发下更友好。防刷标记和验证码分两个 key:防刷标记 60 秒过期,验证码 5 分钟过期,互不影响,用户至少得等 60 秒才能重新发送。

4.2 状态回调:把“发出去”变成“送达了”

短信接口返回000000只代表华为云接受了请求,不代表用户一定收到了短信。要精确掌握送达状态,必须配置状态回调。

在控制台或请求参数里配置回调地址后,华为云会在短信状态变化时回调这个接口。回调报文是 JSON 数组,包含短信 ID、状态码等信息。

后端处理回调:

@RestController public class SmsCallbackController { @Resource private SmsRecordService recordService; @PostMapping("/sms/callback") public void receive(@RequestBody List<SmsCallbackItem> items) { for (SmsCallbackItem item : items) { recordService.updateStatus( item.getSmsId(), item.getStatus(), item.getDescription()); } } }

回调接口建议独立成一个 Controller,不要和业务接口混在一起。一是方便排查问题,二是防止业务代码改动时不小心影响回调接收。生产环境里回调地址一定是公网可访问的 HTTPS 地址,测试环境可以用内网穿透临时调试。

4.3 联调时最容易忽略的测试点

  • 用真实手机号测试。文档示例号或虚拟号可能被运营商限制,收发都会异常。
  • 模板变量个数和内容严格对应。模板里${1}对应参数数组第一个元素,位置不能错。
  • 测试时注意频率限制。频繁给同一号码发验证码会被华为云限流,严重的话需要申诉解封,联调阶段别为了验证接口反复点发送。
  • 验证码发送成功但不入库,用户永远都验证不过。先确认 Redis 里能不能读到刚写的 key,再排查其他环节。

5. 常见问题与排查技巧实录

这部分是实践中最有价值的内容。我直接按“现象—原因—解法”的顺序整理,全是自己或同事真实遇到过的。

5.1 鉴权失败:401 或 403

这是最高频的错误,原因几乎都集中在 WSSE 头的构造上。可能性从高到低排列:

  1. App Secret 用错,而不是 App Key。
  2. HMAC 的 key 和 data 顺序反了。
  3. created用了本地时间而不是 UTC 时间。
  4. nonce不唯一,或者带了非法字符。
  5. X-WSSE头拼写有误,比如多了空格或漏了引号。

排查技巧:写一个最简的 main 方法,把buildWsseHeader的输出打印出来,然后用文档里的示例参数手算一次 Base64 输出。如果你手算的结果和接口返回的错误不一致,说明你的加密算法错了;如果手算结果正确但接口还是报鉴权失败,那就是请求头格式或密钥本身的问题,逐个排查。

5.2 发送成功但手机收不到

用排除法:

  1. 登录华为云控制台,查看“发送记录”,确认该条短信的实际状态。
  2. 如果状态是“送达”但用户没收到,让用户检查手机安全软件是否拦截,或者是否填错了号码。
  3. 如果签名是刚审核通过的新签名,部分通道对接可能有延迟。
  4. 确认发送号码不是运营商黑名单号码。

还有一个容易忽略的点:to字段如果传了带区号的号码,比如+8613800138000,部分接口会解析失败或直接忽略,表现就是“发送成功”但实际没有真实下发。国内号码建议只传 11 位纯数字,如果业务需要支持海外号码,单独处理区号逻辑。

5.3 模板变量解析失败

报错信息里一般带模板关键字,比如Invalid template parameter。逐一核对:

  • 模板里变量是${1},不是{1}也不是${1}(多空格)。
  • 传入的templateParas是 JSON 数组格式的字符串。
  • 数组长度不小于模板变量个数。
  • 数组每个元素都是字符串,不要传数字类型。

举例:模板是您的验证码为${1},${2}分钟内有效,参数必须是["123456","5"]。如果传了[123456, 5]这种非字符串格式,服务端大概率报错。这个格式问题的根因,往往是对接时直接把前端数值塞进了参数,忘了包一层字符串。

5.4 被限流了

短时间内对同一手机号频繁发送,会触发华为云的频率限制,接口返回类似Limit exceeded的错误码。

解法是:

  1. 到控制台查看该应用的配额和限流策略。
  2. 业务层主动降频,把防刷逻辑加上,别全靠平台兜底。
  3. 确实需要高频触达的场景,考虑申请独立通道或调整模板类型。

我在生产上固定的规则是:同一用户验证码 60 秒一次、同一个用户 24 小时最多 10 条、同一个 IP 每小时 20 次。用 Redis 实现成本很低,却能有效避免账号被平台拉进风控名单,这个规则组合我实测下来够用了。

5.5 回调验签问题

严格来说,华为云回调报文里带有签名信息,需要对回调来源做校验,防止伪造。

如果你的回调接口暴露在公网,至少要做一层认证校验。我在项目里用固定 token 加自定义请求头的简单方案,代码量不多,但能挡住常见的脚本扫描。短信回调一旦被伪造,攻击者可能把验证码状态批量改成“成功”,后果不只是数据不准,还可能绕过整个校验流程。

下面是问题速查表,排查时直接对号入座:

现象常见原因解决方向
401/403 鉴权失败Secret 错误、HMAC 顺序反、时间非 UTC检查密钥与 WSSE 构造
发送成功但收不到号码黑名单、新签名延迟、区号问题控制台发送记录确认
模板参数错误变量格式或个数不匹配核对${1}与 JSON 数组
频率超限同一号码高频发送业务层加 Redis 防刷
回调未触发回调地址不可达或未配置检查 HTTPS 与编码格式

最后分享一个我在这次接入中体会最深的事:短信通道不出问题的时候没人关心,一出问题就是大事。所以代码里该打的日志一定要打,尤其是 requestId、smsId 和回调状态。我习惯把短信发送记录落到一张独立的表里,每次发送都记一条,包括请求参数、接口返回、回调状态三个维度的信息。排查“用户说没收到短信”这类问题时,翻这张表比翻聊天记录靠谱得多。

另外还有个小技巧:验证码模板里加上“如非本人操作,请忽略本短信”这类提示语,审核通过率高,用户体验也会好一些。短信这东西,安全感和合规感比什么都重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询