diff --git a/fast-boot-module/fast-boot-message/src/main/java/net/maku/message/controller/SmsPlatformController.java b/fast-boot-module/fast-boot-message/src/main/java/net/maku/message/controller/SmsPlatformController.java index c451e3e..2a32079 100644 --- a/fast-boot-module/fast-boot-message/src/main/java/net/maku/message/controller/SmsPlatformController.java +++ b/fast-boot-module/fast-boot-message/src/main/java/net/maku/message/controller/SmsPlatformController.java @@ -20,18 +20,18 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** -* 短信平台 -* -* @author 阿沐 babamu@126.com -*/ + * 短信平台 + * + * @author 阿沐 babamu@126.com + */ @RestController @RequestMapping("message/sms/platform") -@Tag(name="短信平台") +@Tag(name = "短信平台") @AllArgsConstructor public class SmsPlatformController { private final SmsPlatformService smsPlatformService; @@ -40,7 +40,7 @@ public class SmsPlatformController { @GetMapping("page") @Operation(summary = "分页") @PreAuthorize("hasAuthority('sms:platform:page')") - public Result> page(@Valid SmsPlatformQuery query){ + public Result> page(@Valid SmsPlatformQuery query) { PageResult page = smsPlatformService.page(query); return Result.ok(page); @@ -49,7 +49,7 @@ public class SmsPlatformController { @GetMapping("{id}") @Operation(summary = "信息") @PreAuthorize("hasAuthority('sms:platform:info')") - public Result get(@PathVariable("id") Long id){ + public Result get(@PathVariable("id") Long id) { SmsPlatformEntity entity = smsPlatformService.getById(id); return Result.ok(SmsPlatformConvert.INSTANCE.convert(entity)); @@ -58,7 +58,7 @@ public class SmsPlatformController { @PostMapping @Operation(summary = "保存") @PreAuthorize("hasAuthority('sms:platform:save')") - public Result save(@RequestBody SmsPlatformVO vo){ + public Result save(@RequestBody SmsPlatformVO vo) { smsPlatformService.save(vo); return Result.ok(); @@ -67,13 +67,13 @@ public class SmsPlatformController { @PostMapping("send") @Operation(summary = "发送短信") @PreAuthorize("hasAuthority('sms:platform:update')") - public Result send(@RequestBody SmsSendVO vo){ + public Result send(@RequestBody SmsSendVO vo) { SmsPlatformEntity entity = smsPlatformService.getById(vo.getId()); SmsConfig config = SmsPlatformConvert.INSTANCE.convert2(entity); // 短信参数 - Map params = new HashMap<>(); - if(!StringUtils.isAnyBlank(vo.getParamKey(), vo.getParamValue())) { + Map params = new LinkedHashMap<>(); + if (StringUtils.isNotBlank(vo.getParamValue())) { params.put(vo.getParamKey(), vo.getParamValue()); } @@ -85,7 +85,7 @@ public class SmsPlatformController { smsService.saveLog(config, vo.getMobile(), params, null); return Result.ok(); - }catch (Exception e) { + } catch (Exception e) { // 保存日志 smsService.saveLog(config, vo.getMobile(), params, e); @@ -96,7 +96,7 @@ public class SmsPlatformController { @PutMapping @Operation(summary = "修改") @PreAuthorize("hasAuthority('sms:platform:update')") - public Result update(@RequestBody @Valid SmsPlatformVO vo){ + public Result update(@RequestBody @Valid SmsPlatformVO vo) { smsPlatformService.update(vo); return Result.ok(); @@ -105,7 +105,7 @@ public class SmsPlatformController { @DeleteMapping @Operation(summary = "删除") @PreAuthorize("hasAuthority('sms:platform:delete')") - public Result delete(@RequestBody List idList){ + public Result delete(@RequestBody List idList) { smsPlatformService.delete(idList); return Result.ok(); diff --git a/fast-boot-module/fast-boot-message/src/main/java/net/maku/message/sms/HuaweiSmsStrategy.java b/fast-boot-module/fast-boot-message/src/main/java/net/maku/message/sms/HuaweiSmsStrategy.java new file mode 100644 index 0000000..7894408 --- /dev/null +++ b/fast-boot-module/fast-boot-message/src/main/java/net/maku/message/sms/HuaweiSmsStrategy.java @@ -0,0 +1,195 @@ +package net.maku.message.sms; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.CharsetUtil; +import lombok.Data; +import net.maku.framework.common.exception.FastException; +import net.maku.framework.common.utils.JsonUtils; +import net.maku.message.sms.config.SmsConfig; +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.*; + +/** + * 华为云短信 + * + * @author 阿沐 babamu@126.com + */ +public class HuaweiSmsStrategy implements 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\""; + private final SmsConfig smsConfig; + + public HuaweiSmsStrategy(SmsConfig smsConfig) { + this.smsConfig = smsConfig; + } + + @Override + public void send(String mobile, Map params) { + // APP接入地址(在控制台"应用管理"页面获取)+接口访问URI + String url = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1"; + + // 有参数则设置 + String templateParas = null; + if (MapUtil.isNotEmpty(params)) { + templateParas = JsonUtils.toJsonString(params.values().toArray(new String[0])); + } + + // 请求Body,不携带签名名称时,signature请填null + String body = buildRequestBody(smsConfig.getSenderId(), "+86" + mobile, smsConfig.getTemplateId(), templateParas, null, smsConfig.getSignName()); + if (StringUtils.isBlank(body)) { + throw new FastException("body is null."); + } + + // 请求Headers中的X-WSSE参数值 + String wsseHeader = buildWsseHeader(smsConfig.getAccessKey(), smsConfig.getSecretKey()); + if (StringUtils.isBlank(wsseHeader)) { + throw new FastException("wsse header is null."); + } + + try { + // 使用 https + trustAllHttpsCertificates(); + + URL realUrl = new URL(url); + 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 FastException(result.description); + } + } else { //400 401 + throw new FastException(IoUtil.read(connection.getErrorStream(), CharsetUtil.CHARSET_UTF_8)); + } + } catch (Exception e) { + throw new FastException("短信发送失败:", e); + } + } + + /** + * 构造请求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 FastException("buildRequestBody(): sender, receiver or templateId is null."); + } + Map 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 FastException("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(); + } + + String passwordDigestBase64Str = Base64.getEncoder().encodeToString(passwordDigest); + + 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 result; + } +} diff --git a/fast-boot-module/fast-boot-message/src/main/java/net/maku/message/sms/SmsContext.java b/fast-boot-module/fast-boot-message/src/main/java/net/maku/message/sms/SmsContext.java index de083a0..5ce726a 100644 --- a/fast-boot-module/fast-boot-message/src/main/java/net/maku/message/sms/SmsContext.java +++ b/fast-boot-module/fast-boot-message/src/main/java/net/maku/message/sms/SmsContext.java @@ -7,7 +7,7 @@ import net.maku.message.sms.config.SmsConfig; import java.util.Map; /** - * 短信 Context + * 短信 Context * * @author 阿沐 babamu@126.com */ @@ -15,11 +15,15 @@ public class SmsContext { private final SmsStrategy smsStrategy; public SmsContext(SmsConfig config) { - if(config.getPlatform() == SmsPlatformEnum.ALIYUN.getValue()) { + if (config.getPlatform() == SmsPlatformEnum.ALIYUN.getValue()) { this.smsStrategy = new AliyunSmsStrategy(config); - }else if(config.getPlatform() == SmsPlatformEnum.QCLOUD.getValue()) { + } else if (config.getPlatform() == SmsPlatformEnum.QCLOUD.getValue()) { this.smsStrategy = new QcloudSmsStrategy(config); - }else { + } else if (config.getPlatform() == SmsPlatformEnum.QINIU.getValue()) { + this.smsStrategy = new QiniuSmsStrategy(config); + } else if (config.getPlatform() == SmsPlatformEnum.HUAWEI.getValue()) { + this.smsStrategy = new HuaweiSmsStrategy(config); + } else { throw new FastException("未知的短信平台"); } }