跳到内容

SMS Starter

受众:开发者 摘要:统一短信 Facade。log / memory / smsbao / sms4j 适配的腾讯云 / 阿里云 / 华为 / 云片 / 极光等 23 个 provider。模板渲染 + 后台配置覆盖 + 扁平 fallback + 可插拔 SmsRouteResolver 路由 SPI。

何时使用

场景建议
验证码 / 通知类直发Sms.send(phone, content)Sms.send(provider, SmsMessage)
后台可查询 / 可重试的业务短信Notificationsms 通道
多服务商 / 场景路由高级可选;实现 SmsRouteResolver SPI,按国家 / 地区 / 场景 / 成本在代码侧选 provider
主备 fallback默认 provider / fallback-providers 失败后按顺序切换
短信模板(本地)SmsMessage.template(走 template
厂商审核模板SmsMessage.templateCode(走厂商 ID)
本地调试不发实际短信log provider(默认)
自动化测试断言memory provider

业务代码只依赖 Sms Facade / SmsManager / SmsProvider直接绑定 sms4j 或具体云厂商 SDK。

快速开始

java
// 默认 provider,自由文本
Sms.send("13800138000", "验证码 123456");

// 指定 provider + 模板
Sms.send("tencent", SmsMessage.builder()
        .phone("13800138000")
        .templateCode("123456")           // 厂商审核模板 ID
        .variable("code", "9527")
        .variable("expire", "5")
        .build());

配置最小,只需要一个默认 provider。腾讯云、阿里云等云厂商自身支持国际短信时,直接发送 E.164 手机号即可,不需要为每个国家单独配置路由。

yaml
spring:
  open:
    sms:
      default-provider: tencent
      providers:
        tencent:
          enabled: true
          access-key-id: ${TENCENT_SMS_SECRET_ID:}
          access-key-secret: ${TENCENT_SMS_SECRET_KEY:}
          sdk-app-id: ${TENCENT_SMS_SDK_APP_ID:}
          signature: ${TENCENT_SMS_SIGNATURE:}
          template-id: ${TENCENT_SMS_TEMPLATE_ID:}
          territory: ap-guangzhou

Facade API

java
// 默认 provider
Sms.send(String phone, String content);
Sms.send(SmsMessage message);

// 指定 provider
Sms.send(String provider, SmsMessage message);

// 批量发送
Sms.sendBatch(List<SmsMessage> messages);

SmsMessage 关键字段:

字段说明
phone收件手机号(可多次调用累加)
template框架本地模板编码(template
templateCode厂商审核模板 ID(云厂商)
variable(k, v)模板变量;本地模板(template)渲染和厂商模板(templateCode)发送共用同一袋
attribute(k, v)透传扩展属性;路由元数据 region / countryCode / scene 走这里(key 见 SmsAttributeKeys
content自由文本(推荐生产使用)
locale模板语言,用于本地模板渲染,也可供 SmsRouteResolver 路由
signName短信签名
provider显式指定 provider;设置后优先于 SmsRouteResolver 和默认 provider,且不再自动切换

路由元数据不作 SmsMessage 一等字段,统一进 attributes,由 SmsRouteResolver 按需读取,保持消息对象精简:

属性 key(SmsAttributeKeys字符串值说明
SCENEsms.scenelogin / register / password-reset业务场景
COUNTRY_CODEsms.country-code86 / 1国家或地区电话区号
REGIONsms.regionCN / USISO 3166-1 alpha-2 地区码

Template 边界template(本地)和 templateCode(厂商)不要混用。验证码 / 通知类短信优先走厂商审核模板。

配置项

日常项目推荐保持下面这种简单结构:default-provider 指向一个主 provider,国际号码由服务商和 E.164 手机号处理。fallback-providers 是可选主备列表,差异化路由由 SmsRouteResolver SPI 在代码侧决定,配置里不堆匹配规则。

yaml
spring:
  open:
    sms:
      enabled: true
      default-provider: log
      register-common-providers: true
      config:
        enabled: true                     # 数据库配置覆盖
        prefix: spring.open.sms
      providers:
        tencent:
          enabled: true
          supplier: tencent
          config-id: tencent-main
          access-key-id: ${TENCENT_SMS_SECRET_ID:}
          access-key-secret: ${TENCENT_SMS_SECRET_KEY:}
          sdk-app-id: ${TENCENT_SMS_SDK_APP_ID:}
          signature: ${TENCENT_SMS_SIGNATURE:}
          template-id: ${TENCENT_SMS_TEMPLATE_ID:}
          territory: ap-guangzhou

通用参数

配置默认值说明
enabledtrue启用开关
default-providerlog默认 provider
register-common-providerstrue自动注册常见服务商 provider
config.enabledtrue数据库覆盖本地配置
config.prefixspring.open.sms后台配置 key 前缀
fallback-providers全局后备 provider;默认 provider 或 SmsRouteResolver provider 发送失败后按顺序尝试

Provider 公共字段

字段说明
enabled启停当前 provider
suppliersms4j supplier 名(alibaba / tencent 等)
config-id当前账号配置 ID;多账号唯一
access-key-idAccessKeyId / SecretId / AppId
access-key-secretAccessKeySecret / SecretKey / AppSecret
sdk-app-id短信应用 ID(腾讯 / 容联)
signature已审核签名
template-id默认模板 ID
request-url覆盖请求地址
region-id地域(阿里 / 天翼)
territory国家码(腾讯,默认 ap-guangzhou
properties.<name>厂商差异字段(camelCase setter 名)

log / memory provider

配置默认值说明
providers.log.enabledtrue输出日志,本地开发用
providers.memory.enabledtrue内存记录,测试样板用
providers.memory.capacity1000内存最多保留条数

smsbao provider

配置默认值说明
providers.smsbao.username短信宝用户名
providers.smsbao.password接口密码 / 密钥
providers.smsbao.product-id产品 ID
providers.smsbao.request-urlhttps://api.smsbao.com/sms接口地址

短信宝官方接口是 GET,账号密码作为查询参数 u / p 传输(p 是密钥,不是明文登录密码)。请确认接口走 HTTPS,并避免在反向代理 / 网关把完整 URL 记进访问日志,防止凭据在日志中留痕。

短信路由(SmsRouteResolver SPI)

短信路由是高级可选能力。多数项目只需要一个腾讯云 / 阿里云 provider,它们在账号能力开通、签名和模板审核完成后可以直接发送国际号码;框架只负责把手机号规范化为 E.164 并传给 provider。

SmsManager.send(SmsMessage) 按以下顺序确定候选 provider,去重后依次尝试,第一条成功结果立即返回:

  1. SmsMessage.provider(显式指定,自动切换)
  2. 所有 SmsRouteResolver.resolveProviders(message) 返回的候选(按 Bean 顺序组合)
  3. spring.open.sms.default-provider
  4. spring.open.sms.fallback-providers

候选 provider 返回失败结果或抛出运行时异常都会进入下一个候选。全部失败时返回最后一次失败结果;没有失败结果但最后一次为异常时抛出异常。显式传入 SmsMessage.provider 或调用 Sms.send(provider, message) 时只用该 provider,不自动切换,避免调用方明确指定的短信通道被静默替换。

需要按国家 / 地区、语言、场景或 provider 可用性 / 成本 / 风控选择 provider 时,注册一个或多个 SmsRouteResolver Bean 即可,框架会按 Spring @Order / Ordered 顺序组合候选 provider。某个 resolver 返回空表示跳过,抛出运行时异常时会记录警告并继续执行后续 resolver,避免单个高级路由器影响默认 provider 发送链路。

java
@Bean
@Order(10)
SmsRouteResolver smsRouteResolver() {
    return message -> {
        Map<String, Object> attrs = message.getAttributes();
        if ("US".equalsIgnoreCase((String) attrs.get(SmsAttributeKeys.REGION))) {
            return List.of("twilio", "aliyun");      // 海外走 Twilio,失败回退阿里云国际
        }
        if ("login".equals(attrs.get(SmsAttributeKeys.SCENE))) {
            return List.of("aliyun");                // 登录验证码走阿里云
        }
        return List.of();                            // 其余交给 default-provider
    };
}

SmsRouteResolver 是单一函数式接口,入参是完整 SmsMessage(可读 locale / phones,以及 attributes 里的 region / countryCode / scene,key 见 SmsAttributeKeys),返回按优先级排序的 provider 名称;返回 null 或空表示不路由。多个 resolver 的结果会按顺序合并后统一去重。差异化逻辑全在代码里,可单测,配置层只保留 default-providerfallback-providers

国家码推断、号码前缀解析等策略由 resolver 自行决定。需要从 E.164 手机号推断国家码时,建议在 resolver 内复用 libphonenumber,框架内置脆弱的字符串前缀匹配。

验证码短信经 Notification sms 通道发送时,会把 sms.scene / sms.country-code / sms.region 透传到 SmsMessageSmsRouteResolver 可直接读取。认证验证码发送 / 校验、短信登录、注册、找回密码和绑定手机号都支持可选 countryCode / region,服务端会统一解析为 E.164,再把国家区号和地区透传给短信路由。

认证短信会使用当前请求 Locale 写入 Notification,再透传给 SMS starter。这样本地模板渲染、短信路由和后续厂商模板选择都能按 Accept-Language / 当前语言上下文工作。

后台配置同名 key 优先级 > 环境变量 > application.yml

Provider

Provider说明
log默认,本地开发输出日志
memory测试用,内存保留
smsbao短信宝官方 HTTP(走 sms4j)
23 个云厂商 providersms4j 适配,见下表

SMS 模块边界已经表达「短信」语义,服务商 provider 再追加 -sms 后缀。环境变量可以继续用 TENCENT_SMS_* 这类业务前缀避免和同厂商存储 / 支付混淆。

sms4j 是部分服务商 provider 的内部实现生态作业务可见 provider 名,作配置层级。

同一个 sms4j supplier 可以配置多个业务 provider,例如 tencent-cloud-maintencent-cloud-backup 都使用 supplier: tencent。未显式配置 config-id 时,框架默认使用 provider 名作为 sms4j configId,避免同 supplier 多账号互相覆盖缓存;需要复用外部 sms4j 实例或手动切换账号时,再显式设置 config-id 或在 SmsMessage.attributes 中传 SmsAttributeKeys.PROVIDER_CONFIG_ID

sms4j 适配的内置 provider

Providersupplier推荐必填可选差异字段(properties.*
aliyunalibabaaccess-key-id / access-key-secret / signature / template-idregion-id / request-url / templateName / action / version
tencenttencentaccess-key-id / access-key-secret / sdk-app-id / signature / template-idterritory / request-url / connTimeout / action / version / service
huaweihuaweiaccess-key-id / access-key-secret / template-idsender / statusCallBack / url
baidubaiduaccess-key-id / access-key-secret / signature / template-idhost / action / templateName / custom / userExtId
budingyunbuding_v2access-key-id / access-key-secret / template-idsignKey / args
chuanglanchuanglanaccess-key-id / access-key-secret / signaturebaseUrl / msgUrl
cloopencloopenaccess-key-id / access-key-secret / sdk-app-id / template-idbaseUrl
ctyunctyunaccess-key-id / access-key-secret / signature / template-idrequest-url / templateName / action
danmidanmiaccess-key-id / access-key-secret / template-idhost / action
dingzhongdingzhongaccess-key-id / access-key-secret / signature / template-idrequest-url / templateName / baseAction / templateAction
emayemayaccess-key-id / access-key-secret / template-idrequest-url
jdcloudjdcloudaccess-key-id / access-key-secret / template-idregion
jiguangjiguangaccess-key-id / access-key-secret / template-idrequest-url / signId / action / templateName / voice / ttl / tag
lianlulianluaccess-key-id / access-key-secret / template-idrequest-url / mchId / version / appKey / appId / signType
luosimaoluosimaoaccess-key-idhost / action
masmasaccess-key-id / access-key-secret / signature / template-idrequest-url / ecName / action / addSerial
neteaseneteaseaccess-key-id / access-key-secret / template-idtemplateName / templateUrl / codeUrl / verifyUrl / needUp
qiniuqiniuaccess-key-id / access-key-secret / template-idbaseUrl / templateName / singleMsgUrl / massMsgUrl / signatureId
submailmysubmailaccess-key-id / access-key-secret / template-idhost / action / signType / signVersion
unismsunismsaccess-key-id / access-key-secret / template-idisSimple / templateName
yixintongyixintongaccess-key-id / access-key-secret / template-idrequest-url / spCode / signCode / f
yunpianyunpianaccess-key-id / access-key-secret / signature / template-idcallbackUrl / templateName
zhutongzhutongaccess-key-id / access-key-secret / signature / template-idrequest-url / templateName

properties.* key 用 sms4j 厂商配置类的 Java 属性名(camelCase)。框架会把 String / Integer / Boolean / String[] setter 写入底层配置;数组用英文逗号分隔。

短信宝不在 sms4j 3.3.5 provider 包内,按官方 HTTP 接口参数 u/p/m/c 轻量适配。接口参考:https://smsbao.com/openapi/213.html

自定义 provider

实现 SmsProvider SPI 注册 Bean 即可新增 provider。新增 provider 时优先评估 sms4j provider 映射,只有成熟生态无法满足才自研。

源码机制骨架

类型位置说明
FacadeSms静态入口,委托 SmsManagerProvider 获取当前 SmsManager,业务模块不直接依赖 sms4j
自动装配SmsAutoConfiguration组装配置解析器、用户自定义 provider、内置 log / memory / smsbao provider、sms4j 委托 provider 和路由组合器
ManagerDefaultSmsManager固定执行 本地模板渲染 → 校验 → 路由解析 → default / fallback provider;显式 provider 不自动切换
路由CompositeSmsRouteResolver按 Spring @Order / Ordered 串联多个 SmsRouteResolver,单个 resolver 异常只记录告警
配置解析SmsConfigResolver读取 spring.open.sms.*providers.*,Config starter 可用时同名后台配置优先于本地配置
运行时配置ConfigManagerSmsRuntimeConfigSource只桥接 ConfigManager 的 key 查询,配置存储、缓存和刷新策略留给 Config starter
sms4j 适配DelegatingSmsProvider / DefaultSms4jClient业务 provider 名映射到 sms4j supplier;同 supplier 多账号通过 config-id 隔离
短信宝适配SmsBaoProvider按官方 HTTP GET 参数 u/p/m/c/pid 适配,不经过 sms4j
本地 providerLogSmsProvider / MemorySmsProviderlog 用于本地可观察输出,memory 用于测试断言和轻量预览

扩展时优先保持这条边界:业务只看 provider 名、模板和路由语义;第三方 SDK、supplier、configId 和账号刷新都是 starter 内部细节。

发送日志和失败重试

Sms Facade 是底层入口:创建后台发送日志表,提供失败重试。

后台可查 / 可重试场景走 Notificationsms 通道:

java
Notification.send("sms", NotificationMessage.builder()
        .recipient(NotificationRecipient.mobile("13800138000"))
        .attribute("sms.template-code", "123456")
        .variable("code", "9527")
        .build());
API说明
GET /api/v1/notification-send-logs?channel=sms查询短信发送日志
GET /api/v1/notification-send-logs/summary?channel=sms短信发送结果统计
POST /api/v1/notification-send-logs/{id}/retry重试单条失败
POST /api/v1/notification-send-logs/retry-batch批量重试

POST /api/v1/auth/verify/sms-codes 验证码接口也走 Notification sms 通道(验证码哈希写 Cache,校验成功后立即消费)。验证码短信也能在 Notification 发送日志中查询和重试。

配置示例:单一 provider

yaml
spring:
  open:
    sms:
      default-provider: tencent
      providers:
        tencent:
          enabled: true
          access-key-id: ${TENCENT_SMS_SECRET_ID:}
          access-key-secret: ${TENCENT_SMS_SECRET_KEY:}
          sdk-app-id: ${TENCENT_SMS_SDK_APP_ID:}
          signature: ${TENCENT_SMS_SIGNATURE:}
          template-id: ${TENCENT_SMS_TEMPLATE_ID:}
          territory: ap-guangzhou

如果直接调用 Sms.send("tencent", message) 或把 default-provider 改为 tencent,但没有配置该 provider 的密钥,框架会提示需要配置:

text
spring.open.sms.providers.tencent.access-key-id
spring.open.sms.providers.tencent.access-key-secret

阿里云同理使用 spring.open.sms.providers.aliyun.*。如果项目已经在外部自行注册 sms4j 实例,也可以由外部实例承接发送;业务代码不需要感知 sms4j。

配置示例:多 provider + fallback + 路由 SPI(高级可选)

配置只声明 provider 和全局主备链;差异化路由放在 SmsRouteResolver Bean。

yaml
spring:
  open:
    sms:
      default-provider: tencent
      fallback-providers:
        - aliyun
        - smsbao
      providers:
        tencent:
          enabled: true
          access-key-id: ${TENCENT_SMS_SECRET_ID:}
          access-key-secret: ${TENCENT_SMS_SECRET_KEY:}
          sdk-app-id: ${TENCENT_SMS_SDK_APP_ID:}
          signature: ${TENCENT_SMS_SIGNATURE:}
          template-id: ${TENCENT_SMS_TEMPLATE_ID:}
        aliyun:
          enabled: true
          supplier: alibaba
          access-key-id: ${ALIYUN_SMS_ACCESS_KEY_ID:}
          access-key-secret: ${ALIYUN_SMS_ACCESS_KEY_SECRET:}
          signature: ${ALIYUN_SMS_SIGNATURE:}
          template-id: ${ALIYUN_SMS_TEMPLATE_ID:}
          region-id: cn-hangzhou
        smsbao:
          enabled: true
          username: ${SMSBAO_USERNAME:}
          password: ${SMSBAO_PASSWORD:}
java
// 按地区 / 场景选 provider;其余交给 default-provider + fallback-providers
@Bean
SmsRouteResolver smsRouteResolver() {
    return message -> {
        if ("login".equals(message.getAttributes().get(SmsAttributeKeys.SCENE))) {
            return List.of("aliyun");
        }
        return List.of();
    };
}

后台数据库配置常用 key:

text
spring.open.sms.default-provider
spring.open.sms.fallback-providers[0]
spring.open.sms.fallback-providers[1]
spring.open.sms.providers.tencent.access-key-id
spring.open.sms.providers.tencent.access-key-secret
spring.open.sms.providers.tencent.sdk-app-id
spring.open.sms.providers.tencent.signature
spring.open.sms.providers.tencent.template-id
spring.open.sms.providers.tencent.territory

数据库配置同名 key 优先级高于本地文件,不需要重启业务代码。配置刷新实时生效取决于 Config starter 的刷新链路和缓存设置。

国际化短信

短信能力按国际化场景设计,假设中国大陆手机号 / 单签名 / 单模板。

手机号规范

  • 服务端统一按 E.164 语义保存和比较(例如 +8613800138000
  • 服务端已经接入 libphonenumber 统一规范化 mobile:去除空格、横线、括号等常见分隔符,保留前导 +,把 00 国际冠码转换为 +,并按默认地区 CN 输出 E.164
  • API 继续兼容 mobile 字段,同时认证短信相关入口可接收可选 countryCode / region,由服务端按用户选择地区规范化
  • 号码解析、校验、格式化和默认地区判断优先使用 Google libphonenumber;简单正则只作入参格式兜底
  • 日志、审计、Notification 发送日志和错误消息只展示脱敏号或 hash

短信路由

  • 通过 SmsRouteResolver SPI 按国家 / 地区、locale、场景、provider 可用性 / 成本 / 风控选择 provider
  • 可以注册多个 SmsRouteResolver Bean;框架按 @Order / Ordered 组合,返回候选 provider 后统一去重
  • 国内和国际可用不同 provider;全局 fallback-providers 做主备切换
  • 日常国际短信推荐保持一个默认云厂商 provider + fallback-providers;只有国家 / 地区 / 场景差异明显时才写 resolver
  • resolver 入参是完整 SmsMessage,可读 locale / phonesattributes 里的 region / countryCode / scene(key 见 SmsAttributeKeys);国家码推断(如复用 libphonenumber)由 resolver 自行决定
  • 路由属于 SMS starter 边界,业务代码继续只调 Sms Facade 或 Notification sms 通道

模板语言

  • 验证码、注册、找回、绑定手机号等场景用稳定 scene
  • 认证短信会透传当前请求 Locale、解析后的国家区号和地区,本地模板渲染和短信路由都可以按语言、地区和场景区分
  • 厂商模板、签名、本地模板按 provider + region + locale + scene 选择
  • 变量名稳定(code / expireMinutes / scene),便于多语言模板和厂商审核模板复用
  • 短信正文、错误消息、验证码提示走 i18n / Template,在业务代码写死中英文

前端约定

  • App / PC / Admin 手机号登录、注册、找回、绑定 / 换绑统一国家 / 地区选择器
  • 默认地区可来自用户偏好、站点配置、Accept-Language、IP 或设备环境
  • 前端负责友好输入和展示;规范化、判重、限流、路由由后端

开发约定

  • 在业务模块直接引入 sms4j API
  • 密钥优先环境变量或后台配置,写入代码仓库
  • 验证码 / 通知优先用厂商审核模板(templateCode),自由文本仅作兜底
  • 后台可查 / 可重试场景走 Notification sms 通道
  • 新增服务商优先评估 sms4j provider 映射,自研 provider 是最后选择
  • 短信正文、错误消息走 i18n / Template,硬编码

验证命令

bash
./mvnw -pl spring-open-starters/spring-open-starter-sms -am test -DskipITs
./mvnw -pl spring-open-core/spring-open-core-iam,spring-open-starters/spring-open-starter-sms -am -Dtest='*Sms*Test' -Dsurefire.failIfNoSpecifiedTests=false test -DskipITs
./mvnw -pl spring-open-application -am test -DskipITs -DskipFrontend

参考资料

相关

Released under the Apache License 2.0.