最近项目开发完了,整理梳理了一下框架,总结记录一下通用消息组件的设计。
项目中的消息的存在种类多样性:APP消息、短信、邮件、站内消息,短信的服务商又有多种。我们项目采用的是spirng cloud alibaba,于是把消息这块抽出来做成一个微服务,考虑到消息的共用性,并且可能在同一时间使用多种消息的推送。把消息接口封装成了同一个,接口的实现采用工厂方法模式,便于扩展消息的种类,扩展消息的多服务商。
0.各服务商开通及参数的获取(略)
1.接口设计
package sino.base.service;
import sino.base.events.models.SmsParam;
/**
*
* 短信发送业务
*/
public interface CommonSmsService {
/**
* 异步短信接口
* 重发短信 只需要传smsid 和 userid
* @return
*/
void sendMsgAsync(SmsParam param);
/**
* 同步短信接口
* 重发短信 只需要传smsid 和 userid
* @return
*/
boolean sendMsgSync(SmsParam param);
}
package sino.base.events.models;
import lombok.Data;
/**
*@ClassName SmsParam
*@Description TODO
*@Author xc
*@Date 2024/9/3 15:26
*@Version 1.0
*/
@Data
public class SmsParam {
//重发短信 只需要传smsid 和 userid
private String smsid;
/**
* 标题: email 使用
*/
private String title;
/**
* 电话号码或者邮箱 逗号分隔
*/
private String tels;
/**
* //若platform=hw,
* content为模板参数,规则为: "参数1_参数2" 参数下划线分隔 。
*/
private String content;
// 参数是否拆分 默认:是
// private String contentSplit="1";
private String addSerial;
/**
* 短信平台:移动 cm;华为云 hw 邮箱: email
*/
private String platform;
private String userid;
/**
* 模板编号
*
*/
private String templateno;
private String customParam;
}
2.组件创建
创建通用组件父类
package sino.base.events.models;
import java.io.IOException;
/**
*@ClassName SmsProperties
*@Description TODO
*@Author xc
*@Date 2024/9/3 15:31
*@Version 1.0
*/
public class SmsComponent {
public Boolean sendSms() throws IOException {
return null;
}
}
创建消息子类:华为短信子类、移动云短信子类、163邮箱消息子类、app短信(省略)、站内信息(省略)
package sino.base.events.models;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import com.cloud.apigateway.sdk.utils.Client;
import com.cloud.apigateway.sdk.utils.Request;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
/**
* @ClassName 华为短信
* @Description TODO
* @Author xc
* @Date 2024/9/3 15:28
* @Version 1.0
*/
@Data
@Slf4j
public class HuaWeiSmsComponent extends SmsComponent {
private String smsUrl = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1";
private String appKey = "";
private String appSecret = "";
private String sender;
private String receiver;
private String templateId;
private String templateParas;
private String statusCallBack="";
private String signature;
private String extend;
public HuaWeiSmsComponent(String sender, String receiver, String templateId, String templateParas, String statusCallBack, String signature, String extend) throws Exception {
log.info("参数:sender="+sender+",receiver="+receiver+",templateId="+templateId+",templateParas="+templateParas+",statusCallBack="+statusCallBack+",signature="+signature);
if (null == sender || null == receiver || null == templateId || sender.isEmpty() || receiver.isEmpty()
|| templateId.isEmpty()) {
throw new Exception();
}
this.sender=sender;
this.receiver=receiver;
this.templateId=templateId;
this.templateParas=templateParas;
this.signature=signature;
this.extend=extend;
}
String buildRequestBody() throws UnsupportedEncodingException {
StringBuilder body = new StringBuilder();
appendToBody(body, "from=", sender);
appendToBody(body, "&to=", receiver);
appendToBody(body, "&templateId=", templateId);
appendToBody(body, "&templateParas=", templateParas);
appendToBody(body, "&statusCallback=", statusCallBack);
appendToBody(body, "&signature=", signature);
appendToBody(body, "&extend=", extend);
return body.toString();
}
private void appendToBody(StringBuilder body, String key, String val) throws UnsupportedEncodingException {
if (null != val && !val.isEmpty()) {
log.info("Print appendToBody: {}:{}", key, val);
body.append(key).append(URLEncoder.encode(val, "UTF-8"));
}
}
Request getRequest() throws UnsupportedEncodingException {
Request request = new Request();
request.setKey(appKey);
request.setSecret(appSecret);
request.setMethod("POST");
request.addHeader("Content-Type", "application/x-www-form-urlencoded");
request.setUrl(smsUrl);
request.setBody(buildRequestBody());
return request;
}
@Override
public Boolean sendSms() throws IOException {
CloseableHttpClient client = null;
try {
// Sign the request.
HttpRequestBase signedRequest = Client.sign(getRequest(), Constant.SIGNATURE_ALGORITHM_SDK_HMAC_SHA256);
// Do not verify ssl certificate
client = (CloseableHttpClient) SSLCipherSuiteUtil.createHttpClient(Constant.INTERNATIONAL_PROTOCOL);
HttpResponse response = client.execute(signedRequest);
// Print the body of the response.
HttpEntity resEntity = response.getEntity();
if (resEntity != null) {
String resultMsg = EntityUtils.toString(resEntity, "UTF-8");
log.info("华为短信发送结果:"+resultMsg);
boolean result = resultMsg.indexOf("\"code\":\"000000\",\"description\":\"Success\"") > 0;
return result;
}
} catch (Exception e) {
log.error(e.getMessage());
} finally {
if (client != null) {
client.close();
}
}
return false;
}
}
package sino.base.events.models;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONObject;
import sino.util.StringUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import sun.misc.BASE64Encoder;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
/**
*@ClassName 中国移动短信
*@Description TODO
*@Author xc
*@Date 2024/9/3 15:28
*@Version 1.0
*/
@Data
@Slf4j
public class CmSmsComponent extends SmsComponent {
private String smsUrl="";
private String ecName="";
private String sign="";
private String apId="";
private String secretKey="";
private String tels;
private String content;
private String addSerial="";
public CmSmsComponent(String tels, String content, String addSerial) {
this.tels = tels;
this.content = content;
if(StringUtil.isNotEmpty(addSerial)){
this.addSerial = addSerial;
}
}
public String getTemporary(){
String temporary = ecName+apId+secretKey+tels+content+sign+addSerial;
return temporary;
}
public String post() throws Exception {
String jsonParam=getEncodeStr3();
HttpClient httpClient = new DefaultHttpClient();
HttpPost post = new HttpPost(smsUrl);
StringEntity postingString = new StringEntity(jsonParam);
post.setEntity(postingString);
HttpResponse response = httpClient.execute(post);
String content = EntityUtils.toString(response.getEntity());
System.out.println(content);
return content;
}
public String encrypt32(String encryptStr) {
MessageDigest md5;
try {
md5 = MessageDigest.getInstance("MD5");
byte[] md5Bytes = md5.digest(encryptStr.getBytes("UTF-8"));
StringBuffer hexValue = new StringBuffer();
for (int i = 0; i < md5Bytes.length; i++) {
int val = ((int) md5Bytes[i]) & 0xff;
if (val < 16) {
hexValue.append("0");
}
hexValue.append(Integer.toHexString(val));
}
encryptStr = hexValue.toString();
} catch (Exception e) {
throw new RuntimeException(e);
}
return encryptStr;
}
public String getEncodeStr3() throws Exception {
String temporary = getTemporary();
System.out.println("temporary:"+temporary);
//MD5加密
String mac = encrypt32(temporary) ;
System.out.println("mac_MD5:"+mac);
// 存放要进行加密的信息
Map<String,String> params=new HashMap<String,String>();
params.put("ecName",ecName);
params.put("apId",apId);
params.put("mobiles",tels);
params.put("content",content);
params.put("sign",sign);
params.put("addSerial",addSerial);
params.put("mac",mac);
BASE64Encoder base64 = new BASE64Encoder();
String encodeStr3 = base64.encode(JSONObject.toJSONString(params).getBytes("UTF-8"));
return encodeStr3;
}
@Override
public Boolean sendSms(){
try {
String ret = post();
cn.hutool.json.JSONObject jsonObject= JSONUtil.parseObj(ret);
log.info("短信发送结果:"+ret);
if(jsonObject!=null&&"success".equalsIgnoreCase(jsonObject.get("rspcod").toString())){
return true;
}
} catch (Exception e) {
log.error(e.getMessage(),e);
}
return false;
}
}
package sino.base.events.models;
import cn.hutool.extra.mail.MailAccount;
import cn.hutool.extra.mail.MailUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
/**
* @ClassName 163邮箱
* @Author xc
* @Date 2024/9/3 15:28
* @Version 1.0
*/
@Data
@Slf4j
public class Email163Component extends SmsComponent {
private String secret = "";
private String from = "";
private String to;
private String title;
private String content;
public Email163Component(String to, String title, String content) {
this.to=to;
this.title=title;
this.content=content;
}
@Override
public Boolean sendSms() {
try {
MailAccount mailAccount = new MailAccount();
mailAccount.setFrom(from);//你的邮箱
mailAccount.setPass(secret);// 授权码
MailUtil.send(mailAccount, to, title, content, false);
} catch (Exception e) {
return false;
}
return true;
}
}
3.接口实现
逻辑:1.发送短信 2.记录短信发送日志
package sino.base.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import sino.base.entity.SmsMessageEntity;
import sino.base.entity.SmsRulersHwEntity;
import sino.base.events.handlers.SmsEvent;
import sino.base.events.models.*;
import sino.base.service.CommonSmsService;
import sino.base.service.SmsMessageService;
import sino.base.service.SmsRulersHwService;
import sino.util.RandomUtil;
import sino.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import java.util.Date;
/**
*
*短信发送
*/
@Service
@Slf4j
public class CommonSmsServiceImpl implements CommonSmsService {
@Autowired
private ApplicationContext context;
@Autowired
private SmsMessageService smsMessageService;
@Autowired
private SmsRulersHwService smsRulersHwService;
@Override
public void sendMsgAsync(SmsParam param) {
log.info("进入异步发短信接口,参数 = " + param.toString());
SmsEvent event=new SmsEvent(param);
context.publishEvent(event);
}
@Override
public boolean sendMsgSync(SmsParam param) {
log.info("进入同步发短信接口,参数 = " + param.toString());
return sendMsg(param);
}
public Boolean sendMsg(SmsParam param){
try {
SmsMessageEntity smsMessageEntity;
String tels;
String context;
String serial;
String templateNo;
if (StringUtil.isNotEmpty(param.getSmsid())) {
smsMessageEntity = smsMessageService.getInfo(param.getSmsid());
tels = smsMessageEntity.getTels();
context = smsMessageEntity.getContent();
serial = smsMessageEntity.getSerial();
templateNo=smsMessageEntity.getExt1();
int count = Integer.parseInt(smsMessageEntity.getCount()) + 1;
smsMessageEntity.setCount(count + "");
smsMessageEntity.setUpdatetime(new Date());
smsMessageEntity.setUpdateuser(param.getUserid());
} else {
tels = param.getTels();
context = param.getContent();
serial = param.getAddSerial();
templateNo=param.getTemplateno();
smsMessageEntity = new SmsMessageEntity();
smsMessageEntity.setCount(1 + "");
smsMessageEntity.setCreateuser(param.getUserid());
smsMessageEntity.setTels(param.getTels());
smsMessageEntity.setContent(param.getContent());
smsMessageEntity.setPlatform(param.getPlatform());
smsMessageEntity.setSendtime(new Date());
smsMessageEntity.setSerial(param.getAddSerial());
}
smsMessageEntity.setExt1(templateNo);
smsMessageEntity.setSmsid(RandomUtil.uuId());
smsMessageEntity.setExt2(param.getCustomParam());
String platform = param.getPlatform();
if (StringUtil.isEmpty(platform)) {
platform = "hw";
}
SmsComponent smsComponent;
if (platform.equals("cm")) {
smsComponent = new CmSmsComponent(tels, context, serial);
} else if(platform.equals("hw")){
SmsRulersHwEntity ruler= smsRulersHwService.getOne(new QueryWrapper<SmsRulersHwEntity>().eq("templateno",templateNo));
if(null==ruler){
return false;
}
smsComponent = new HuaWeiSmsComponent(ruler.getSender(),param.getTels(),ruler.getTemplateid(),getParamsContext(param.getContent()),"",ruler.getSignature(),smsMessageEntity.getSmsid());
}else if(platform.equals("email")){
smsComponent= new Email163Component(tels,param.getTitle(),context);
}else{
//smsComponent = new HuaWeiSmsComponent("8822030413721",param.getTels(),"827f3b01dcf04fd5bc7657b6d575fdd3","[\"666666\"]","","");
SmsRulersHwEntity ruler= smsRulersHwService.getOne(new QueryWrapper<SmsRulersHwEntity>().eq("templateno",templateNo));
smsComponent = new HuaWeiSmsComponent(ruler.getSender(),param.getTels(),ruler.getTemplateid(),param.getContent(),"",ruler.getSignature(),smsMessageEntity.getSmsid());
}
smsMessageEntity.setPlatform(platform);
Boolean result = smsComponent.sendSms();
log.info("短信发送结果 = " + result);
//记录短信日志
if (result) {
smsMessageEntity.setSendstatus("成功");
} else {
smsMessageEntity.setSendstatus("失败");
}
smsMessageService.saveOrUpdate(smsMessageEntity);
return result;
} catch (Exception e) {
log.error("保存发送短信出错", e);
}
return false;
}
private String getParamsContext(String params){
String msgParams="";
String[] paramsArr= params.split("_");
for (String p:paramsArr) {
if(!msgParams.equals("")){
msgParams=msgParams+",";
}
msgParams+="\""+p+"\"";
}
msgParams="["+msgParams+"]";
return msgParams;
}
}
4.其他需要的工具类
/*
* Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved.
*/
package sino.base.events.models;
import okhttp3.OkHttpClient;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.bouncycastle.crypto.BlockCipher;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.prng.SP800SecureRandomBuilder;
import org.openeuler.BGMProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.*;
import java.security.cert.X509Certificate;
import java.util.Locale;
public class SSLCipherSuiteUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(SSLCipherSuiteUtil.class);
private static CloseableHttpClient httpClient;
private static OkHttpClient okHttpClient;
private static final int CIPHER_LEN = 256;
private static final int ENTROPY_BITS_REQUIRED = 384;
public static HttpClient createHttpClient(String protocol) throws Exception {
SSLContext sslContext = getSslContext(protocol);
// create factory
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext,
new String[]{protocol}, Constant.SUPPORTED_CIPHER_SUITES, new TrustAllHostnameVerifier());
httpClient = HttpClients.custom().setSSLSocketFactory(sslConnectionSocketFactory).build();
return httpClient;
}
// public static OkHttpClient createOkHttpClient(String protocol) throws Exception {
// SSLContext sslContext = getSslContext(protocol);
// // Create an ssl socket factory with our all-trusting manager
// SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
// OkHttpClient.Builder builder = new OkHttpClient.Builder()
// .sslSocketFactory(sslSocketFactory, new TrustAllManager())
// .hostnameVerifier(new TrustAllHostnameVerifier());
// okHttpClient = builder.connectTimeout(10, TimeUnit.SECONDS).readTimeout(60, TimeUnit.SECONDS).build();
// return okHttpClient;
// }
public static HttpURLConnection createHttpsOrHttpURLConnection(URL uUrl, String protocol) throws Exception {
// initial connection
if (uUrl.getProtocol().toUpperCase(Locale.getDefault()).equals(Constant.HTTPS)) {
SSLContext sslContext = getSslContext(protocol);
HttpsURLConnection.setDefaultHostnameVerifier(new TrustAllHostnameVerifier());
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
return (HttpsURLConnection) uUrl.openConnection();
}
return (HttpURLConnection) uUrl.openConnection();
}
private static SSLContext getSslContext(String protocol) throws Exception,
NoSuchAlgorithmException, NoSuchProviderException, KeyManagementException {
if (!Constant.GM_PROTOCOL.equals(protocol) && !Constant.INTERNATIONAL_PROTOCOL.equals(protocol)) {
LOGGER.info("Unsupport protocol: {}, Only support GMTLS TLSv1.2", protocol);
throw new Exception("Unsupport protocol, Only support GMTLS TLSv1.2");
}
// Create a trust manager that does not validate certificate chains
TrustAllManager[] trust = {new TrustAllManager()};
KeyManager[] kms = null;
SSLContext sslContext;
sslContext = SSLContext.getInstance(Constant.INTERNATIONAL_PROTOCOL, "SunJSSE");
if (Constant.GM_PROTOCOL.equals(protocol)) {
Security.insertProviderAt(new BGMProvider(), 1);
sslContext = SSLContext.getInstance(Constant.GM_PROTOCOL, "BGMProvider");
}
SecureRandom secureRandom = getSecureRandom();
sslContext.init(kms, trust, secureRandom);
sslContext.getServerSessionContext().setSessionCacheSize(8192);
sslContext.getServerSessionContext().setSessionTimeout(3600);
return sslContext;
}
// 不校验域名
private static class TrustAllHostnameVerifier implements HostnameVerifier {
public boolean verify(String hostname, SSLSession session) {
return true;
}
}
// 不校验服务端证书
private static class TrustAllManager implements X509TrustManager {
private X509Certificate[] issuers;
public TrustAllManager() {
this.issuers = new X509Certificate[0];
}
public X509Certificate[] getAcceptedIssuers() {
return issuers;
}
public void checkClientTrusted(X509Certificate[] chain, String authType) {
}
public void checkServerTrusted(X509Certificate[] chain, String authType) {
}
}
private static SecureRandom getSecureRandom() {
SecureRandom source;
try {
source = SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException e) {
LOGGER.error("get SecureRandom failed", e);
throw new RuntimeException("get SecureRandom failed");
}
boolean predictionResistant = true;
BlockCipher cipher = new AESEngine();
boolean reSeed = false;
return new SP800SecureRandomBuilder(source, predictionResistant).setEntropyBitsRequired(
ENTROPY_BITS_REQUIRED).buildCTR(cipher, CIPHER_LEN, null, reSeed);
}
}
/*
* Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved.
*/
package sino.base.events.models;
public final class Constant {
public static final String HTTPS = "HTTPS";
public static final String GM_PROTOCOL = "GMTLS";
public static final String INTERNATIONAL_PROTOCOL = "TLSv1.2";
public static final String SIGNATURE_ALGORITHM_SDK_HMAC_SHA256 = "SDK-HMAC-SHA256";
public static final String SIGNATURE_ALGORITHM_SDK_HMAC_SM3 = "SDK-HMAC-SM3";
public static final String[] SUPPORTED_CIPHER_SUITES = {"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"};
private Constant() {
}
}
5.相关依赖
<!-- 华为短信依赖 -->
<dependency>
<groupId>com.huawei.apigateway</groupId>
<artifactId>java-sdk-core</artifactId>
<version>3.2.4</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.1</version>
</dependency>
<dependency>
<groupId>org.openeuler</groupId>
<artifactId>bgmprovider</artifactId>
<version>1.0.4</version>
</dependency>
<!-- 邮箱 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
6.后续待优化的地方
①参数名称设置统一
②组件的构造设置成统一参数
③接口实现的 方法还可以再拆封封装一下