SMS Starter
受众:开发者 摘要:统一短信 Facade。
log/memory/smsbao/ sms4j 适配的腾讯云 / 阿里云 / 华为 / 云片 / 极光等 23 个 provider。模板渲染 + 后台配置覆盖 + 扁平 fallback + 可插拔SmsRouteResolver路由 SPI。
何时使用
| 场景 | 建议 |
|---|---|
| 验证码 / 通知类直发 | Sms.send(phone, content) 或 Sms.send(provider, SmsMessage) |
| 后台可查询 / 可重试的业务短信 | 走 Notification 的 sms 通道 |
| 多服务商 / 场景路由 | 高级可选;实现 SmsRouteResolver SPI,按国家 / 地区 / 场景 / 成本在代码侧选 provider |
| 主备 fallback | 默认 provider / fallback-providers 失败后按顺序切换 |
| 短信模板(本地) | SmsMessage.template(走 template) |
| 厂商审核模板 | SmsMessage.templateCode(走厂商 ID) |
| 本地调试不发实际短信 | log provider(默认) |
| 自动化测试断言 | memory provider |
业务代码只依赖 Sms Facade / SmsManager / SmsProvider,不直接绑定 sms4j 或具体云厂商 SDK。
快速开始
// 默认 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 手机号即可,不需要为每个国家单独配置路由。
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-guangzhouFacade API
// 默认 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) | 字符串值 | 说明 |
|---|---|---|
SCENE(sms.scene) | login / register / password-reset | 业务场景 |
COUNTRY_CODE(sms.country-code) | 86 / 1 | 国家或地区电话区号 |
REGION(sms.region) | CN / US | ISO 3166-1 alpha-2 地区码 |
Template 边界:template(本地)和 templateCode(厂商)不要混用。验证码 / 通知类短信优先走厂商审核模板。
配置项
日常项目推荐保持下面这种简单结构:default-provider 指向一个主 provider,国际号码由服务商和 E.164 手机号处理。fallback-providers 是可选主备列表,差异化路由由 SmsRouteResolver SPI 在代码侧决定,配置里不堆匹配规则。
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通用参数
| 配置 | 默认值 | 说明 |
|---|---|---|
enabled | true | 启用开关 |
default-provider | log | 默认 provider |
register-common-providers | true | 自动注册常见服务商 provider |
config.enabled | true | 数据库覆盖本地配置 |
config.prefix | spring.open.sms | 后台配置 key 前缀 |
fallback-providers | 空 | 全局后备 provider;默认 provider 或 SmsRouteResolver provider 发送失败后按顺序尝试 |
Provider 公共字段
| 字段 | 说明 |
|---|---|
enabled | 启停当前 provider |
supplier | sms4j supplier 名(alibaba / tencent 等) |
config-id | 当前账号配置 ID;多账号唯一 |
access-key-id | AccessKeyId / SecretId / AppId |
access-key-secret | AccessKeySecret / 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.enabled | true | 输出日志,本地开发用 |
providers.memory.enabled | true | 内存记录,测试样板用 |
providers.memory.capacity | 1000 | 内存最多保留条数 |
smsbao provider
| 配置 | 默认值 | 说明 |
|---|---|---|
providers.smsbao.username | 空 | 短信宝用户名 |
providers.smsbao.password | 空 | 接口密码 / 密钥 |
providers.smsbao.product-id | 空 | 产品 ID |
providers.smsbao.request-url | https://api.smsbao.com/sms | 接口地址 |
短信宝官方接口是 GET,账号密码作为查询参数
u/p传输(p是密钥,不是明文登录密码)。请确认接口走 HTTPS,并避免在反向代理 / 网关把完整 URL 记进访问日志,防止凭据在日志中留痕。
短信路由(SmsRouteResolver SPI)
短信路由是高级可选能力。多数项目只需要一个腾讯云 / 阿里云 provider,它们在账号能力开通、签名和模板审核完成后可以直接发送国际号码;框架只负责把手机号规范化为 E.164 并传给 provider。
SmsManager.send(SmsMessage) 按以下顺序确定候选 provider,去重后依次尝试,第一条成功结果立即返回:
SmsMessage.provider(显式指定,不自动切换)- 所有
SmsRouteResolver.resolveProviders(message)返回的候选(按 Bean 顺序组合) spring.open.sms.default-providerspring.open.sms.fallback-providers
候选 provider 返回失败结果或抛出运行时异常都会进入下一个候选。全部失败时返回最后一次失败结果;没有失败结果但最后一次为异常时抛出异常。显式传入 SmsMessage.provider 或调用 Sms.send(provider, message) 时只用该 provider,不自动切换,避免调用方明确指定的短信通道被静默替换。
需要按国家 / 地区、语言、场景或 provider 可用性 / 成本 / 风控选择 provider 时,注册一个或多个 SmsRouteResolver Bean 即可,框架会按 Spring @Order / Ordered 顺序组合候选 provider。某个 resolver 返回空表示跳过,抛出运行时异常时会记录警告并继续执行后续 resolver,避免单个高级路由器影响默认 provider 发送链路。
@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-provider 和 fallback-providers。
国家码推断、号码前缀解析等策略由 resolver 自行决定。需要从 E.164 手机号推断国家码时,建议在 resolver 内复用 libphonenumber,框架不内置脆弱的字符串前缀匹配。
验证码短信经 Notification sms 通道发送时,会把 sms.scene / sms.country-code / sms.region 透传到 SmsMessage,SmsRouteResolver 可直接读取。认证验证码发送 / 校验、短信登录、注册、找回密码和绑定手机号都支持可选 countryCode / region,服务端会统一解析为 E.164,再把国家区号和地区透传给短信路由。
认证短信会使用当前请求 Locale 写入 Notification,再透传给 SMS starter。这样本地模板渲染、短信路由和后续厂商模板选择都能按 Accept-Language / 当前语言上下文工作。
后台配置同名 key 优先级 > 环境变量 > application.yml。
Provider
| Provider | 说明 |
|---|---|
log | 默认,本地开发输出日志 |
memory | 测试用,内存保留 |
smsbao | 短信宝官方 HTTP(不走 sms4j) |
| 23 个云厂商 provider | sms4j 适配,见下表 |
SMS 模块边界已经表达「短信」语义,服务商 provider 不再追加 -sms 后缀。环境变量可以继续用 TENCENT_SMS_* 这类业务前缀避免和同厂商存储 / 支付混淆。
sms4j 是部分服务商 provider 的内部实现生态,不作业务可见 provider 名,不作配置层级。
同一个 sms4j supplier 可以配置多个业务 provider,例如 tencent-cloud-main 和 tencent-cloud-backup 都使用 supplier: tencent。未显式配置 config-id 时,框架默认使用 provider 名作为 sms4j configId,避免同 supplier 多账号互相覆盖缓存;需要复用外部 sms4j 实例或手动切换账号时,再显式设置 config-id 或在 SmsMessage.attributes 中传 SmsAttributeKeys.PROVIDER_CONFIG_ID。
sms4j 适配的内置 provider
| Provider | supplier | 推荐必填 | 可选差异字段(properties.*) |
|---|---|---|---|
aliyun | alibaba | access-key-id / access-key-secret / signature / template-id | region-id / request-url / templateName / action / version |
tencent | tencent | access-key-id / access-key-secret / sdk-app-id / signature / template-id | territory / request-url / connTimeout / action / version / service |
huawei | huawei | access-key-id / access-key-secret / template-id | sender / statusCallBack / url |
baidu | baidu | access-key-id / access-key-secret / signature / template-id | host / action / templateName / custom / userExtId |
budingyun | buding_v2 | access-key-id / access-key-secret / template-id | signKey / args |
chuanglan | chuanglan | access-key-id / access-key-secret / signature | baseUrl / msgUrl |
cloopen | cloopen | access-key-id / access-key-secret / sdk-app-id / template-id | baseUrl |
ctyun | ctyun | access-key-id / access-key-secret / signature / template-id | request-url / templateName / action |
danmi | danmi | access-key-id / access-key-secret / template-id | host / action |
dingzhong | dingzhong | access-key-id / access-key-secret / signature / template-id | request-url / templateName / baseAction / templateAction |
emay | emay | access-key-id / access-key-secret / template-id | request-url |
jdcloud | jdcloud | access-key-id / access-key-secret / template-id | region |
jiguang | jiguang | access-key-id / access-key-secret / template-id | request-url / signId / action / templateName / voice / ttl / tag |
lianlu | lianlu | access-key-id / access-key-secret / template-id | request-url / mchId / version / appKey / appId / signType |
luosimao | luosimao | access-key-id | host / action |
mas | mas | access-key-id / access-key-secret / signature / template-id | request-url / ecName / action / addSerial |
netease | netease | access-key-id / access-key-secret / template-id | templateName / templateUrl / codeUrl / verifyUrl / needUp |
qiniu | qiniu | access-key-id / access-key-secret / template-id | baseUrl / templateName / singleMsgUrl / massMsgUrl / signatureId |
submail | mysubmail | access-key-id / access-key-secret / template-id | host / action / signType / signVersion |
unisms | unisms | access-key-id / access-key-secret / template-id | isSimple / templateName |
yixintong | yixintong | access-key-id / access-key-secret / template-id | request-url / spCode / signCode / f |
yunpian | yunpian | access-key-id / access-key-secret / signature / template-id | callbackUrl / templateName |
zhutong | zhutong | access-key-id / access-key-secret / signature / template-id | request-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 映射,只有成熟生态无法满足才自研。
源码机制骨架
| 类型 | 位置 | 说明 |
|---|---|---|
| Facade | Sms | 静态入口,委托 SmsManagerProvider 获取当前 SmsManager,业务模块不直接依赖 sms4j |
| 自动装配 | SmsAutoConfiguration | 组装配置解析器、用户自定义 provider、内置 log / memory / smsbao provider、sms4j 委托 provider 和路由组合器 |
| Manager | DefaultSmsManager | 固定执行 本地模板渲染 → 校验 → 路由解析 → 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 |
| 本地 provider | LogSmsProvider / MemorySmsProvider | log 用于本地可观察输出,memory 用于测试断言和轻量预览 |
扩展时优先保持这条边界:业务只看 provider 名、模板和路由语义;第三方 SDK、supplier、configId 和账号刷新都是 starter 内部细节。
发送日志和失败重试
Sms Facade 是底层入口:不创建后台发送日志表,不提供失败重试。
后台可查 / 可重试场景走 Notification 的 sms 通道:
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
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 的密钥,框架会提示需要配置:
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。
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:}// 按地区 / 场景选 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:
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
短信路由
- 通过
SmsRouteResolverSPI 按国家 / 地区、locale、场景、provider 可用性 / 成本 / 风控选择 provider - 可以注册多个
SmsRouteResolverBean;框架按@Order/Ordered组合,返回候选 provider 后统一去重 - 国内和国际可用不同 provider;全局
fallback-providers做主备切换 - 日常国际短信推荐保持一个默认云厂商 provider +
fallback-providers;只有国家 / 地区 / 场景差异明显时才写 resolver - resolver 入参是完整
SmsMessage,可读locale/phones和attributes里的 region / countryCode / scene(key 见SmsAttributeKeys);国家码推断(如复用 libphonenumber)由 resolver 自行决定 - 路由属于 SMS starter 边界,业务代码继续只调
SmsFacade 或 Notificationsms通道
模板语言
- 验证码、注册、找回、绑定手机号等场景用稳定
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,不硬编码
验证命令
./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参考资料
- SMS4J 厂商配置:https://sms4j.com/doc3/supplierConfig.html
- SMS4J Spring Boot:https://sms4j.com/doc3/springBoot.html
- 短信宝开放接口:https://smsbao.com/openapi/213.html
相关
- 统一通知 + 发送日志:notification.md
- 模板与变量:template.md
- 多语言:i18n.md
- 限流 + 验证码:rate-limit.md
- Provider 扩展模型:provider.md