平台官网地址:
因各个平台规则不一样,需查看相应官网文档。文章主要封装简单功能,复杂功能,需要查看文档。
1.pom
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>2.0.18</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.3</version>
</dependency>
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java</artifactId>
<version>3.1.574</version>
</dependency>
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>7.11.0</version>
</dependency>
2.策略模式
import com.lean.sms.properties.SmsProperties;
import java.util.Map;
/**
* 短信
*
* @author
*/
public abstract class SmsStrategy {
public SmsProperties smsProperties;
/**
* 发送短信
* @param mobile 手机号
* @param params 参数
*/
public abstract void send(String mobile, Map<String, String> params);
}
华为云:
package com.lean.sms.strategy;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.CharsetUtil;
import com.lean.sms.properties.SmsProperties;
import com.lean.sms.utils.JsonUtils;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import javax.net.ssl.*;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* 华为云短信
* https://support.huaweicloud.com/productdesc-msgsms/sms_templates.html
* https://support.huaweicloud.com/devg-msgsms/sms_04_0002.html#ZH-CN_TOPIC_0000001174512727__section1551232617020
* @author
*/
public class HuaweiSmsStrategy extends SmsStrategy {
// 无需修改,用于格式化鉴权头域,给"X-WSSE"参数赋值
private static final String WSSE_HEADER_FORMAT = "UsernameToken Username=\"%s\",PasswordDigest=\"%s\",Nonce=\"%s\",Created=\"%s\"";
// 无需修改,用于格式化鉴权头域,给"Authorization"参数赋值
private static final String AUTH_HEADER_VALUE = "WSSE realm=\"SDP\",profile=\"UsernameToken\",type=\"Appkey\"";
public HuaweiSmsStrategy(SmsProperties smsProperties) {
this.smsProperties = smsProperties;
}
@Override
public void send(String mobile, Map<String, String> params) {
// 有参数则设置
String templateParas = null;
if (MapUtil.isNotEmpty(params)) {
templateParas = JsonUtils.toJsonString(params.values().toArray(new String[0]));
}
//必填,全局号码格式(包含国家码),示例:+8615123456789,多个号码之间用英文逗号分隔
String receiver = "+86" + mobile; //短信接收人号码
//选填,短信状态报告接收地址,推荐使用域名,为空或者不填表示不接收状态报告
String statusCallBack = "";
// 请求Body,不携带签名名称时,signature请填null
String body = buildRequestBody(smsProperties.getHuawei().getSenderId(), receiver, smsProperties.getHuawei().getTemplateId(), templateParas, statusCallBack, smsProperties.getHuawei().getSignName());
if (StringUtils.isBlank(body)) {
throw new RuntimeException("body is null.");
}
// 请求Headers中的X-WSSE参数值
String wsseHeader = buildWsseHeader(smsProperties.getConfig().getAccessKey(), smsProperties.getConfig().getSecretKey());
if (StringUtils.isBlank(wsseHeader)) {
throw new RuntimeException("wsse header is null.");
}
try {
// 使用 https
trustAllHttpsCertificates();
URL realUrl = new URL(smsProperties.getHuawei().getUrl());
HttpsURLConnection connection = (HttpsURLConnection) realUrl.openConnection();
HostnameVerifier hv = (hostname, session) -> true;
connection.setHostnameVerifier(hv);
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setUseCaches(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
connection.setRequestProperty("Authorization", AUTH_HEADER_VALUE);
connection.setRequestProperty("X-WSSE", wsseHeader);
connection.connect();
IoUtil.writeUtf8(connection.getOutputStream(), true, body);
int status = connection.getResponseCode();
if (status == HttpStatus.OK.value()) {
String response = IoUtil.read(connection.getInputStream(), CharsetUtil.CHARSET_UTF_8);
HuaweiSmsResult result = JsonUtils.parseObject(response, HuaweiSmsResult.class);
// 短信是否发送成功
assert result != null;
if (!"000000".equals(result.code)) {
throw new RuntimeException(result.description);
}
} else { //400 401
throw new RuntimeException(IoUtil.read(connection.getErrorStream(), CharsetUtil.CHARSET_UTF_8));
}
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
/**
* 构造请求Body体
*
* @param signature | 签名名称,使用国内短信通用模板时填写
*/
static String buildRequestBody(String sender, String receiver, String templateId, String templateParas,
String statusCallBack, String signature) {
if (null == sender || null == receiver || null == templateId || sender.isEmpty() || receiver.isEmpty()
|| templateId.isEmpty()) {
throw new RuntimeException("buildRequestBody(): sender, receiver or templateId is null.");
}
Map<String, String> map = new HashMap<>();
map.put("from", sender);
map.put("to", receiver);
map.put("templateId", templateId);
if (null != templateParas && !templateParas.isEmpty()) {
map.put("templateParas", templateParas);
}
if (null != statusCallBack && !statusCallBack.isEmpty()) {
map.put("statusCallback", statusCallBack);
}
if (null != signature && !signature.isEmpty()) {
map.put("signature", signature);
}
StringBuilder sb = new StringBuilder();
String temp = "";
for (String s : map.keySet()) {
try {
temp = URLEncoder.encode(map.get(s), "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
sb.append(s).append("=").append(temp).append("&");
}
return sb.deleteCharAt(sb.length() - 1).toString();
}
/**
* 构造X-WSSE参数值
*/
static String buildWsseHeader(String appKey, String appSecret) {
if (null == appKey || null == appSecret || appKey.isEmpty() || appSecret.isEmpty()) {
throw new RuntimeException("buildWsseHeader(): appKey or appSecret is null.");
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
String time = sdf.format(new Date());
String nonce = UUID.randomUUID().toString().replace("-", "");
MessageDigest md;
byte[] passwordDigest = null;
try {
md = MessageDigest.getInstance("SHA-256");
md.update((nonce + time + appSecret).getBytes());
passwordDigest = md.digest();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
//如果JDK版本是1.8,请加载原生Base64类,并使用如下代码
String passwordDigestBase64Str = Base64.getEncoder().encodeToString(passwordDigest); //PasswordDigest
//如果JDK版本低于1.8,请加载三方库提供Base64类,并使用如下代码
//String passwordDigestBase64Str = Base64.encodeBase64String(passwordDigest); //PasswordDigest
//若passwordDigestBase64Str中包含换行符,请执行如下代码进行修正
//passwordDigestBase64Str = passwordDigestBase64Str.replaceAll("[\\s*\t\n\r]", "");
return String.format(WSSE_HEADER_FORMAT, appKey, passwordDigestBase64Str, nonce, time);
}
static void trustAllHttpsCertificates() throws Exception {
TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType) {
}
public void checkServerTrusted(X509Certificate[] chain, String authType) {
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
};
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, null);
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
}
@Data
static class HuaweiSmsResult {
// code为000000,表示成功
private String code;
private String description;
private List<Object> result;
}
}
阿里云:
package com.lean.sms.strategy;
import cn.hutool.core.map.MapUtil;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.teaopenapi.models.Config;
import com.lean.sms.properties.SmsProperties;
import com.lean.sms.utils.Constant;
import com.lean.sms.utils.JsonUtils;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
/**
* 阿里云短信
*https://help.aliyun.com/document_detail/71160.htm?spm=a2c4g.11186623.0.0.51d99373cLZqvl
* @author
*/
@Slf4j
public class AliyunSmsStrategy extends SmsStrategy {
private final Client client;
public AliyunSmsStrategy(SmsProperties smsProperties) {
this.smsProperties = smsProperties;
try {
Config config = new Config();
// 您的AccessKey ID
config.setAccessKeyId(smsProperties.getConfig().getAccessKey());
// 您的AccessKey Secret
config.setAccessKeySecret(smsProperties.getConfig().getSecretKey());
config.endpoint = "dysmsapi.aliyuncs.com";
this.client = new Client(config);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void send(String mobile, Map<String, String> params) {
SendSmsRequest request = new SendSmsRequest();
request.setSignName(smsProperties.getAliyun().getSignName());
request.setTemplateCode(smsProperties.getAliyun().getTemplateId());
request.setPhoneNumbers(mobile);
//设置模板参数
if (MapUtil.isNotEmpty(params)) {
request.setTemplateParam(JsonUtils.toJsonString(params));
}
try {
// 发送短信
SendSmsResponse response = client.sendSms(request);
// 发送失败
if (!Constant.OK.equalsIgnoreCase(response.getBody().getCode())) {
throw new RuntimeException(response.getBody().getMessage());
}
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
}
七牛云:
import com.lean.sms.properties.SmsProperties;
import com.qiniu.http.Response;
import com.qiniu.sms.SmsManager;
import com.qiniu.util.Auth;
import java.util.Map;
/**
* 七牛云短信
*https://developer.qiniu.com/sms/5993/sms-sdk
* @author
*/
public class QiniuSmsStrategy extends SmsStrategy {
private final SmsManager smsManager;
public QiniuSmsStrategy(SmsProperties smsProperties) {
this.smsProperties = smsProperties;
Auth auth = Auth.create(smsProperties.getConfig().getAccessKey(), smsProperties.getConfig().getSecretKey());
smsManager = new SmsManager(auth);
}
@Override
public void send(String mobile, Map<String, String> params) {
try {
Response response = smsManager.sendSingleMessage(smsProperties.getQiniu().getTemplateId(), mobile, params);
// 是否发送成功
if (!response.isOK()) {
throw new RuntimeException(response.error);
}
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
}
腾讯云:
package com.lean.sms.strategy;
import cn.hutool.core.map.MapUtil;
import com.lean.sms.properties.SmsProperties;
import com.lean.sms.utils.Constant;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.sms.v20210111.SmsClient;
import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
import com.tencentcloudapi.sms.v20210111.models.SendStatus;
import java.util.Map;
/**
* 腾讯云短信
*https://cloud.tencent.com/document/api/382/52077
* @author
*/
public class TencentSmsStrategy extends SmsStrategy {
private SmsClient client;
public TencentSmsStrategy(SmsProperties smsProperties) {
this.smsProperties = smsProperties;
try {
// 实例化一个http选项,可选的
HttpProfile httpProfile = new HttpProfile();
httpProfile.setReqMethod("POST");
httpProfile.setConnTimeout(30); // 请求连接超时时间,单位为秒(默认60秒)
httpProfile.setWriteTimeout(30); // 设置写入超时时间,单位为秒(默认0秒)
httpProfile.setReadTimeout(30); // 设置读取超时时间,单位为秒(默认0秒)
httpProfile.setEndpoint("sms.tencentcloudapi.com"); // 指定接入地域域名(默认就近接入)
ClientProfile clientProfile = new ClientProfile();
clientProfile.setSignMethod("HmacSHA256"); // 指定签名算法(默认为HmacSHA256)
// 自3.1.80版本开始,SDK 支持打印日志。
clientProfile.setHttpProfile(httpProfile);
Credential cred = new Credential(smsProperties.getConfig().getAccessKey(), smsProperties.getConfig().getSecretKey());
this.client = new SmsClient(cred, "ap-shanghai", clientProfile);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void send(String mobile, Map<String, String> params) {
SendSmsRequest request = new SendSmsRequest();
request.setSmsSdkAppId(smsProperties.getTencent().getAppId());
request.setSignName(smsProperties.getTencent().getSignName());
request.setTemplateId(smsProperties.getTencent().getTemplateId());
// 有参数则设置
if (MapUtil.isNotEmpty(params)) {
request.setTemplateParamSet(params.values().toArray(new String[0]));
}
//采用 E.164 标准,格式为+[国家或地区码][手机号],单次请求最多支持200个手机号且要求全为境内手机号或全为境外手机号。
// 区段 + 手机号
String[] phoneNumberSet = {"+86" + mobile};
request.setPhoneNumberSet(phoneNumberSet);
// 国际、港澳台短信,需要添加SenderId,国内短信填空,默认未开通
request.setSenderId(smsProperties.getTencent().getSenderId());
try {
// 发送短信
SendSmsResponse response = client.SendSms(request);
SendStatus sendStatus = response.getSendStatusSet()[0];
// 发送失败
if (!Constant.OK.equalsIgnoreCase(sendStatus.getCode())) {
throw new RuntimeException(sendStatus.getMessage());
}
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
}
3.配置
import com.lean.sms.enums.SmsPlatformEnum;
import com.lean.sms.properties.SmsProperties;
import com.lean.sms.strategy.*;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 短信 Context
*
* @author
*/
@Configuration
@EnableConfigurationProperties(SmsProperties.class)
@ConditionalOnProperty(prefix = "sms", value = "enabled")
public class SmsConfiguration {
@Bean
public SmsStrategy smsStrategy(SmsProperties smsProperties) {
if (smsProperties.getConfig().getType() == SmsPlatformEnum.ALIYUN) {
return new AliyunSmsStrategy(smsProperties);
} else if (smsProperties.getConfig().getType() == SmsPlatformEnum.TENCENT) {
return new TencentSmsStrategy(smsProperties);
} else if (smsProperties.getConfig().getType() == SmsPlatformEnum.QINIU) {
return new QiniuSmsStrategy(smsProperties);
} else if (smsProperties.getConfig().getType() == SmsPlatformEnum.HUAWEI) {
return new HuaweiSmsStrategy(smsProperties);
} else {
throw new RuntimeException("未知的短信平台");
}
}
}
/**
* 短信平台枚举
*/
@Getter
@AllArgsConstructor
public enum SmsPlatformEnum {
/**
* 阿里云
*/
ALIYUN,
/**
* 腾讯云
*/
TENCENT,
/**
* 七牛云
*/
QINIU,
/**
* 华为云
*/
HUAWEI;
}