add 百度智能云 sms

This commit is contained in:
bleachtred 2024-04-25 20:25:44 +08:00
parent 81341fcf7c
commit b19287cd58
11 changed files with 461 additions and 2 deletions

View File

@ -16,6 +16,7 @@ import org.dromara.sms4j.api.dao.SmsDao;
import org.dromara.sms4j.api.dao.SmsDaoDefaultImpl;
import org.dromara.sms4j.api.universal.SupplierConfig;
import org.dromara.sms4j.api.verify.PhoneVerify;
import org.dromara.sms4j.baidu.config.BaiduFactory;
import org.dromara.sms4j.budingyun.config.BudingV2Factory;
import org.dromara.sms4j.cloopen.config.CloopenFactory;
import org.dromara.sms4j.comm.constant.Constant;
@ -257,6 +258,7 @@ public class SEInitializer {
ProviderFactoryHolder.registerFactory(QiNiuFactory.instance());
ProviderFactoryHolder.registerFactory(BudingV2Factory.instance());
ProviderFactoryHolder.registerFactory(MasFactory.instance());
ProviderFactoryHolder.registerFactory(BaiduFactory.instance());
if (SmsUtils.isClassExists("com.jdcloud.sdk.auth.CredentialsProvider")) {
ProviderFactoryHolder.registerFactory(JdCloudFactory.instance());
}

View File

@ -0,0 +1,53 @@
package org.dromara.sms4j.baidu.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.provider.config.BaseConfig;
/**
* <p>类名: BaiduConfig
* <p>说明百度智能云 sms
*
* @author :bleachhtred
* 2024/4/25 13:40
**/
@Data
@EqualsAndHashCode(callSuper = true)
public class BaiduConfig extends BaseConfig {
/**
* 请求地址
*/
private String host = "https://smsv3.bj.baidubce.com";
/**
* 接口名称
*/
private String action = "/api/v3/sendSms";
/**
* 模板变量名称
*/
private String templateName;
/**
* 用户自定义参数格式为字符串状态回调时会回传该值
*/
private String custom;
/**
* 通道自定义扩展码
*/
private String userExtId;
/**
* 获取供应商
*
* @since 3.0.0
*/
@Override
public String getSupplier() {
return SupplierConstant.BAIDU;
}
}

View File

@ -0,0 +1,49 @@
package org.dromara.sms4j.baidu.config;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.baidu.service.BaiduSmsImpl;
import org.dromara.sms4j.provider.factory.AbstractProviderFactory;
/**
* <p>类名: BaiduFactory
* <p>说明百度智能云 sms
*
* @author :bleachhtred
* 2024/4/25 13:40
**/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class BaiduFactory extends AbstractProviderFactory<BaiduSmsImpl, BaiduConfig> {
private static final BaiduFactory INSTANCE = new BaiduFactory();
/**
* 获取建造者实例
* @return 建造者实例
*/
public static BaiduFactory instance() {
return INSTANCE;
}
/**
* createSms
* <p> 建造一个短信实现对像
*
* @author :bleachhtred
*/
@Override
public BaiduSmsImpl createSms(BaiduConfig baiduConfig) {
return new BaiduSmsImpl(baiduConfig);
}
/**
* 获取供应商
* @return 供应商
*/
@Override
public String getSupplier() {
return SupplierConstant.BAIDU;
}
}

View File

@ -0,0 +1,186 @@
package org.dromara.sms4j.baidu.service;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.comm.delayedTime.DelayedTime;
import org.dromara.sms4j.comm.exception.SmsBlendException;
import org.dromara.sms4j.comm.utils.SmsUtils;
import org.dromara.sms4j.baidu.config.BaiduConfig;
import org.dromara.sms4j.baidu.utils.BaiduUtils;
import org.dromara.sms4j.provider.service.AbstractSmsBlend;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
/**
* <p>类名: BaiduSmsImpl
* <p>说明百度智能云 sms
*
* @author :bleachhtred
* 2024/4/25 13:40
**/
@Slf4j
public class BaiduSmsImpl extends AbstractSmsBlend<BaiduConfig> {
private int retry = 0;
public BaiduSmsImpl(BaiduConfig config, Executor pool, DelayedTime delayedTime) {
super(config, pool, delayedTime);
}
public BaiduSmsImpl(BaiduConfig config) {
super(config);
}
@Override
public String getSupplier() {
return SupplierConstant.BAIDU;
}
@Override
public SmsResponse sendMessage(String phone, String message) {
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
map.put(getConfig().getTemplateName(), message);
return sendMessage(phone, getConfig().getTemplateId(), map);
}
@Override
public SmsResponse sendMessage(String phone, LinkedHashMap<String, String> messages) {
if (CollUtil.isEmpty(messages)){
messages = new LinkedHashMap<>();
}
return sendMessage(phone, getConfig().getTemplateId(), messages);
}
@Override
public SmsResponse sendMessage(String phone, String templateId, LinkedHashMap<String, String> messages) {
if (CollUtil.isEmpty(messages)){
messages = new LinkedHashMap<>();
}
return getSmsResponse(phone, templateId, messages);
}
@Override
public SmsResponse massTexting(List<String> phones, String message) {
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
map.put(getConfig().getTemplateName(), message);
return massTexting(phones, getConfig().getTemplateId(), map);
}
@Override
public SmsResponse massTexting(List<String> phones, String templateId, LinkedHashMap<String, String> messages) {
if (CollUtil.isEmpty(messages)){
messages = new LinkedHashMap<>();
}
return getSmsResponse(SmsUtils.arrayToString(phones), templateId, messages);
}
private SmsResponse getSmsResponse(String phone, String templateId, LinkedHashMap<String, String> messages) {
return getSmsResponseWithClientToken(phone, templateId, messages, null);
}
private void checkClientToken(String clientToken){
if (StrUtil.isBlank(clientToken)){
log.error("clientToken is required.");
throw new SmsBlendException("clientToken is required.");
}
}
public SmsResponse sendMessageWithClientToken(String phone, String message, String clientToken) {
checkClientToken(clientToken);
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
map.put(getConfig().getTemplateName(), message);
return sendMessageWithClientToken(phone, getConfig().getTemplateId(), map, clientToken);
}
public SmsResponse sendMessageWithClientToken(String phone, LinkedHashMap<String, String> messages, String clientToken) {
checkClientToken(clientToken);
if (CollUtil.isEmpty(messages)){
messages = new LinkedHashMap<>();
}
return sendMessageWithClientToken(phone, getConfig().getTemplateId(), messages, clientToken);
}
public SmsResponse sendMessageWithClientToken(String phone, String templateId, LinkedHashMap<String, String> messages, String clientToken) {
checkClientToken(clientToken);
if (CollUtil.isEmpty(messages)){
messages = new LinkedHashMap<>();
}
return getSmsResponseWithClientToken(phone, templateId, messages, clientToken);
}
public SmsResponse massTextingWithClientToken(List<String> phones, String message, String clientToken) {
checkClientToken(clientToken);
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
map.put(getConfig().getTemplateName(), message);
return massTextingWithClientToken(phones, getConfig().getTemplateId(), map, clientToken);
}
public SmsResponse massTextingWithClientToken(List<String> phones, String templateId, LinkedHashMap<String, String> messages, String clientToken) {
checkClientToken(clientToken);
if (CollUtil.isEmpty(messages)){
messages = new LinkedHashMap<>();
}
return getSmsResponseWithClientToken(SmsUtils.arrayToString(phones), templateId, messages, clientToken);
}
private SmsResponse getSmsResponseWithClientToken(String phone, String templateId, LinkedHashMap<String, String> messages, String clientToken) {
BaiduConfig config = getConfig();
if (StrUtil.isBlank(config.getSignature())){
log.error("signatureId is required.");
throw new SmsBlendException("signatureId is required.");
}
if (StrUtil.isBlank(templateId)){
log.error("template is required.");
throw new SmsBlendException("template is required.");
}
if (StrUtil.isBlank(phone)){
log.error("mobile is required.");
throw new SmsBlendException("mobile is required.");
}
Map<String, String> headers;
Map<String, Object> body;
try {
headers = BaiduUtils.buildHeaders(config, clientToken);
body = BaiduUtils.buildBody(phone, templateId, config.getSignature(), messages, config.getCustom(), config.getUserExtId());
} catch (Exception e) {
log.error("baidu sms buildHeaders or buildBody error", e);
throw new SmsBlendException(e.getMessage());
}
SmsResponse smsResponse;
try {
smsResponse = getResponse(http.postJson(config.getHost() + config.getAction(), headers, body));
} catch (SmsBlendException e) {
smsResponse = new SmsResponse();
smsResponse.setSuccess(false);
smsResponse.setData(e.getMessage());
}
if (smsResponse.isSuccess() || retry == config.getMaxRetries()) {
retry = 0;
return smsResponse;
}
return requestRetry(phone, templateId, messages, clientToken);
}
private SmsResponse requestRetry(String phone, String templateId, LinkedHashMap<String, String> messages, String clientToken) {
http.safeSleep(getConfig().getRetryInterval());
retry ++;
log.warn("The SMS has been resent for the {}th time.", retry);
return getSmsResponseWithClientToken(phone, templateId, messages, clientToken);
}
private SmsResponse getResponse(JSONObject resJson) {
SmsResponse smsResponse = new SmsResponse();
smsResponse.setSuccess("1000".equals(resJson.getStr("code")));
smsResponse.setData(resJson);
smsResponse.setConfigId(getConfigId());
return smsResponse;
}
}

View File

@ -0,0 +1,131 @@
package org.dromara.sms4j.baidu.utils;
import cn.hutool.core.net.URLEncodeUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.baidu.config.BaiduConfig;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class BaiduUtils {
private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
/**
* 创建前缀字符串
* @param accessKeyId 访问密钥ID
* @return bce-auth-v1/{accessKeyId}/{timestamp}/{expirationPeriodInSeconds }
*/
private static String authStringPrefix(String accessKeyId){
SDF.setTimeZone(new SimpleTimeZone(0, "GMT"));
return "bce-auth-v1/" + accessKeyId + "/" + SDF.format(new Date()) + "/1800";
}
/**
* 创建规范请求
* @param host Host域
* @param action 接口名称
* @param clientToken 幂等性参数
* @return HTTP Method + "\n" + CanonicalURI + "\n" + CanonicalQueryString + "\n" + CanonicalHeaders
*/
private static String canonicalRequest(String host, String action, String clientToken){
return "POST\n" + canonicalURI(action) + "\n" + canonicalQueryString(clientToken) + "\n" + canonicalHeaders(host);
}
/**
* Formatting the URL with signing protocol.
* @param action URI
* @return UriEncodeExceptSlash
*/
private static String canonicalURI(String action){
return URLEncodeUtil.encode(action, StandardCharsets.UTF_8);
}
/**
* Formatting the query string with signing protocol.
* @param clientToken 幂等性参数
* @return String
*/
private static String canonicalQueryString(String clientToken){
if (StrUtil.isBlank(clientToken)) {
return StrUtil.EMPTY;
}
return "clientToken=" + URLEncodeUtil.encode(clientToken, StandardCharsets.UTF_8);
}
/**
* Formatting the headers from the request based on signing protocol.
* @param host only host
* @return String
*/
private static String canonicalHeaders(String host){
return URLEncodeUtil.encode("host", StandardCharsets.UTF_8) + ":" + URLEncodeUtil.encode(host, StandardCharsets.UTF_8);
}
/**
* HMAC-SHA256-HEX
* @param key 密钥
* @param str 要加密的字符串
* @return 小写形式的十六进制字符串
*/
private static String sha256Hex(String key, String str) {
HMac hMac = new HMac(HmacAlgorithm.HmacSHA256, key.getBytes(StandardCharsets.UTF_8));
return hMac.digestHex(str, StandardCharsets.UTF_8);
}
/**
* 构造 HTTP Headers请求头
* @param config 百度智能云配置
* @param clientToken 幂等性参数
* @return Headers请求头
*/
public static Map<String, String> buildHeaders(BaiduConfig config, String clientToken) {
// 创建前缀字符串
String authStringPrefix = authStringPrefix(config.getAccessKeyId());
// 生成派生密钥
String signingKey = sha256Hex(config.getAccessKeySecret(), authStringPrefix(config.getAccessKeyId()));
// 生成签名摘要及认证字符串
String signature = sha256Hex(signingKey, canonicalRequest(config.getHost(), config.getAction(), clientToken));
// 认证字符串
String authorization = authStringPrefix + "/" + "/" + signature;
Map<String, String> headers = new HashMap<>(2);
headers.put("Authorization", authorization);
headers.put("host", config.getHost());
return headers;
}
/**
* 构造 HTTP Body 请求体
* @param mobile 手机号码 支持单个或多个手机号多个手机号之间以英文逗号分隔
* @param template 短信模板ID模板申请成功后自动创建全局内唯一
* @param signatureId 短信签名ID签名表申请成功后自动创建全局内唯一
* @param contentVar 模板变量内容用于替换短信模板中定义的变量
* @param custom 用户自定义参数格式为字符串状态回调时会回传该值
* @param userExtId 通道自定义扩展码上行回调时会回传该值其格式为纯数字串默认为不开通请求时无需设置该参数如需开通请联系SMS帮助申请
* @return Body 请求体
*/
public static Map<String, Object> buildBody(String mobile, String template, String signatureId,
LinkedHashMap<String, String> contentVar, String custom, String userExtId) {
Map<String, Object> body = new HashMap<>(4);
body.put("mobile", mobile);
body.put("template", template);
body.put("signatureId", signatureId);
body.put("contentVar", contentVar);
if (StrUtil.isNotBlank(custom)){
body.put("custom", custom);
}
if (StrUtil.isNotBlank(userExtId)){
body.put("userExtId", userExtId);
}
return body;
}
}

View File

@ -29,7 +29,7 @@ public class BudingV2SmsImpl extends AbstractSmsBlend<BudingV2Config> {
*/
private int retry = 0;
private final String URL = "https://smsapi.idcbdy.com";
private static final String URL = "https://smsapi.idcbdy.com";
protected BudingV2SmsImpl(BudingV2Config config, Executor pool, DelayedTime delayed) {
super(config, pool, delayed);

View File

@ -1,7 +1,6 @@
package org.dromara.sms4j.mas.service;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;

View File

@ -8,6 +8,7 @@ import org.dromara.sms4j.aliyun.config.AlibabaFactory;
import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.universal.SupplierConfig;
import org.dromara.sms4j.api.verify.PhoneVerify;
import org.dromara.sms4j.baidu.config.BaiduFactory;
import org.dromara.sms4j.budingyun.config.BudingV2Factory;
import org.dromara.sms4j.cloopen.config.CloopenFactory;
import org.dromara.sms4j.comm.constant.Constant;
@ -125,6 +126,7 @@ public class SmsBlendsInitializer {
ProviderFactoryHolder.registerFactory(QiNiuFactory.instance());
ProviderFactoryHolder.registerFactory(BudingV2Factory.instance());
ProviderFactoryHolder.registerFactory(MasFactory.instance());
ProviderFactoryHolder.registerFactory(BaiduFactory.instance());
if(SmsUtils.isClassExists("com.jdcloud.sdk.auth.CredentialsProvider")) {
ProviderFactoryHolder.registerFactory(JdCloudFactory.instance());
}

View File

@ -112,6 +112,17 @@ sms:
template-id:
# 可为空 不为空时请遵守中国移动云MAS开发文档中的描述[服务代码加扩展码总长度不能超过20位。]
add-serial:
# 中百度智能云 sms
baidu:
access-key-id: 访问密钥ID
access-key-secret: 用户密钥
ec-name: 企业名称
signature: 签名编码
template-id: 模板ID
# 模板变量名称
template-name: code
custom: 用户自定义参数,格式为字符串,状态回调时会回传该值 可不传
user-ext-id: 通道自定义扩展码 可不传
sms-oa:
config-type: yaml

View File

@ -2,12 +2,14 @@ package org.dromara.sms4j.example;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.baidu.service.BaiduSmsImpl;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.comm.utils.SmsUtils;
import org.dromara.sms4j.core.factory.SmsFactory;
@ -333,4 +335,26 @@ class Sms4jTest {
log.info(JSONUtil.toJsonStr(listRes));
}
/**
* 百度短信
*/
@Test
public void baiduSmsTest() {
if (StrUtil.isBlank(PHONE)) {
return;
}
// 发送短信
SmsResponse resp = SmsFactory.getBySupplier(SupplierConstant.BAIDU)
.sendMessage(PHONE, SmsUtils.getRandomInt(6));
log.info(JSONUtil.toJsonStr(resp));
// 发送携带幂等性参数短信
BaiduSmsImpl sendWithClientToken = (BaiduSmsImpl) SmsFactory.getBySupplier(SupplierConstant.BAIDU);
String clientToken = UUID.fastUUID().toString(true);
SmsResponse respWithClientToken = sendWithClientToken.sendMessageWithClientToken(PHONE,
SmsUtils.getRandomInt(6),
clientToken);
log.info(JSONUtil.toJsonStr(respWithClientToken));
}
}

View File

@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.aliyun.config.AlibabaFactory;
import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.universal.SupplierConfig;
import org.dromara.sms4j.baidu.config.BaiduFactory;
import org.dromara.sms4j.budingyun.config.BudingV2Factory;
import org.dromara.sms4j.api.verify.PhoneVerify;
import org.dromara.sms4j.cloopen.config.CloopenFactory;
@ -140,6 +141,7 @@ public class SmsBlendsInitializer {
ProviderFactoryHolder.registerFactory(QiNiuFactory.instance());
ProviderFactoryHolder.registerFactory(BudingV2Factory.instance());
ProviderFactoryHolder.registerFactory(MasFactory.instance());
ProviderFactoryHolder.registerFactory(BaiduFactory.instance());
if (SmsUtils.isClassExists("com.jdcloud.sdk.auth.CredentialsProvider")) {
if (SmsUtils.isClassExists("com.jdcloud.sdk.auth.CredentialsProvider")) {
ProviderFactoryHolder.registerFactory(JdCloudFactory.instance());