Commit 8da08072422404b1ee66781102beda41f51701e5

Authored by YunaiV
1 parent af5be1bc

【代码评审】SYSTEM:华为云 client 的 review

yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java
@@ -46,7 +46,6 @@ public class SmsCallbackController { @@ -46,7 +46,6 @@ public class SmsCallbackController {
46 @PostMapping("/huawei") 46 @PostMapping("/huawei")
47 @PermitAll 47 @PermitAll
48 @Operation(summary = "华为云短信的回调", description = "参见 https://support.huaweicloud.com/api-msgsms/sms_05_0003.html 文档") 48 @Operation(summary = "华为云短信的回调", description = "参见 https://support.huaweicloud.com/api-msgsms/sms_05_0003.html 文档")
49 - @OperateLog(enable = false)  
50 public CommonResult<Boolean> receiveHuaweiSmsStatus(HttpServletRequest request) throws Throwable { 49 public CommonResult<Boolean> receiveHuaweiSmsStatus(HttpServletRequest request) throws Throwable {
51 String text = ServletUtils.getBody(request); 50 String text = ServletUtils.getBody(request);
52 smsSendService.receiveSmsStatus(SmsChannelEnum.HUAWEI.getCode(), text); 51 smsSendService.receiveSmsStatus(SmsChannelEnum.HUAWEI.getCode(), text);
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java
1 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; 1 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
2 2
3 -  
4 import cn.hutool.core.lang.Assert; 3 import cn.hutool.core.lang.Assert;
5 import cn.hutool.core.util.HexUtil; 4 import cn.hutool.core.util.HexUtil;
6 import cn.hutool.core.util.StrUtil; 5 import cn.hutool.core.util.StrUtil;
7 -  
8 import cn.hutool.crypto.SecureUtil; 6 import cn.hutool.crypto.SecureUtil;
9 import cn.hutool.crypto.digest.DigestUtil; 7 import cn.hutool.crypto.digest.DigestUtil;
10 import cn.hutool.json.JSONArray; 8 import cn.hutool.json.JSONArray;
@@ -15,34 +13,27 @@ import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespD @@ -15,34 +13,27 @@ import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespD
15 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; 13 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
16 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; 14 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
17 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; 15 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
18 -import org.apache.http.client.methods.*;  
19 -import org.apache.http.entity.StringEntity;  
20 -import org.apache.http.impl.client.CloseableHttpClient;  
21 -import org.apache.http.impl.client.HttpClientBuilder;  
22 -import org.slf4j.Logger;  
23 -import org.slf4j.LoggerFactory;  
24 -  
25 import com.fasterxml.jackson.annotation.JsonFormat; 16 import com.fasterxml.jackson.annotation.JsonFormat;
26 import com.fasterxml.jackson.annotation.JsonProperty; 17 import com.fasterxml.jackson.annotation.JsonProperty;
27 import lombok.Data; 18 import lombok.Data;
28 import lombok.extern.slf4j.Slf4j; 19 import lombok.extern.slf4j.Slf4j;
29 import org.apache.http.HttpResponse; 20 import org.apache.http.HttpResponse;
  21 +import org.apache.http.client.methods.HttpUriRequest;
  22 +import org.apache.http.client.methods.RequestBuilder;
  23 +import org.apache.http.entity.StringEntity;
  24 +import org.apache.http.impl.client.CloseableHttpClient;
  25 +import org.apache.http.impl.client.HttpClientBuilder;
30 26
31 -import java.io.UnsupportedEncodingException;  
32 import java.net.URLEncoder; 27 import java.net.URLEncoder;
33 import java.nio.charset.StandardCharsets; 28 import java.nio.charset.StandardCharsets;
34 import java.text.SimpleDateFormat; 29 import java.text.SimpleDateFormat;
35 -import java.util.*;  
36 -  
37 -  
38 import java.time.LocalDateTime; 30 import java.time.LocalDateTime;
39 - 31 +import java.util.*;
40 32
41 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; 33 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
42 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; 34 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
43 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT; 35 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
44 36
45 -  
46 /** 37 /**
47 * 华为短信客户端的实现类 38 * 华为短信客户端的实现类
48 * 39 *
@@ -56,56 +47,63 @@ public class HuaweiSmsClient extends AbstractSmsClient { @@ -56,56 +47,63 @@ public class HuaweiSmsClient extends AbstractSmsClient {
56 * 调用成功 code 47 * 调用成功 code
57 */ 48 */
58 public static final String API_CODE_SUCCESS = "OK"; 49 public static final String API_CODE_SUCCESS = "OK";
59 - private static final Logger LOGGER = LoggerFactory.getLogger(HuaweiSmsClient.class);  
60 50
61 public HuaweiSmsClient(SmsChannelProperties properties) { 51 public HuaweiSmsClient(SmsChannelProperties properties) {
62 super(properties); 52 super(properties);
63 Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); 53 Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
64 Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); 54 Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
65 } 55 }
  56 +
66 @Override 57 @Override
67 protected void doInit() { 58 protected void doInit() {
68 -  
69 } 59 }
  60 +
70 @Override 61 @Override
71 public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, 62 public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
72 List<KeyValue<String, Object>> templateParams) throws Throwable { 63 List<KeyValue<String, Object>> templateParams) throws Throwable {
  64 + // TODO @scholar:https://smsapi.cn-north-4.myhuaweicloud.com:443 是不是枚举成静态变量
73 String url = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1"; //APP接入地址+接口访问URI 65 String url = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1"; //APP接入地址+接口访问URI
74 // 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构 66 // 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构
75 // 所以将 通道号 拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"。空格为分隔符。 67 // 所以将 通道号 拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"。空格为分隔符。
  68 + // TODO @scholar:暂时只考虑中国大陆,所以不需要 sender 哈
76 String sender = StrUtil.subAfter(apiTemplateId, " ", true); //中国大陆短信签名通道号或全球短信通道号 69 String sender = StrUtil.subAfter(apiTemplateId, " ", true); //中国大陆短信签名通道号或全球短信通道号
77 String templateId = StrUtil.subBefore(apiTemplateId, " ", true); //模板ID 70 String templateId = StrUtil.subBefore(apiTemplateId, " ", true); //模板ID
78 71
79 - //必填,全局号码格式(包含国家码),示例:+86151****6789,多个号码之间用英文逗号分隔  
80 - String receiver = mobile; //短信接收人号码  
81 -  
82 - //选填,短信状态报告接收地址,推荐使用域名,为空或者不填表示不接收状态报告 72 + // 选填,短信状态报告接收地址,推荐使用域名,为空或者不填表示不接收状态报告
83 String statusCallBack = properties.getCallbackUrl(); 73 String statusCallBack = properties.getCallbackUrl();
84 74
  75 + // TODO @scholar:1)是不是用 LocalDateTimeUtil.format();这样 3 行变成一行
  76 + // TODO @scholar:singerDate 叫 sdkDate 会更合适哈,这样理解起来简单。另外,singer 应该是 signed 么?
85 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.ENGLISH); 77 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.ENGLISH);
86 sdf.setTimeZone(TimeZone.getTimeZone("UTC")); 78 sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
87 String singerDate = sdf.format(new Date()); 79 String singerDate = sdf.format(new Date());
88 80
  81 + // TODO @scholar:整个处理加密的过程,是不是应该抽成一个 private 方法哈。这样整个调用的主干更清晰。
89 // ************* 步骤 1:拼接规范请求串 ************* 82 // ************* 步骤 1:拼接规范请求串 *************
90 String httpRequestMethod = "POST"; 83 String httpRequestMethod = "POST";
91 String canonicalUri = "/sms/batchSendSms/v1/"; 84 String canonicalUri = "/sms/batchSendSms/v1/";
92 - String canonicalQueryString = "";//查询参数为空 85 + String canonicalQueryString = ""; // 查询参数为空
93 String canonicalHeaders = "content-type:application/x-www-form-urlencoded\n" 86 String canonicalHeaders = "content-type:application/x-www-form-urlencoded\n"
94 + "host:smsapi.cn-north-4.myhuaweicloud.com:443\n" 87 + "host:smsapi.cn-north-4.myhuaweicloud.com:443\n"
95 + "x-sdk-date:" + singerDate + "\n"; 88 + "x-sdk-date:" + singerDate + "\n";
  89 + // TODO @scholar:静态枚举了
96 String signedHeaders = "content-type;host;x-sdk-date"; 90 String signedHeaders = "content-type;host;x-sdk-date";
97 - /** 91 + // TODO @scholar:下面的注释,可以考虑去掉
  92 + /*
98 * 选填,使用无变量模板时请赋空值 String templateParas = ""; 93 * 选填,使用无变量模板时请赋空值 String templateParas = "";
99 * 单变量模板示例:模板内容为"您的验证码是${NUM_6}"时,templateParas可填写为"[\"111111\"]" 94 * 单变量模板示例:模板内容为"您的验证码是${NUM_6}"时,templateParas可填写为"[\"111111\"]"
100 * 双变量模板示例:模板内容为"您有${NUM_2}件快递请到${TXT_20}领取"时,templateParas可填写为"[\"3\",\"人民公园正门\"]" 95 * 双变量模板示例:模板内容为"您有${NUM_2}件快递请到${TXT_20}领取"时,templateParas可填写为"[\"3\",\"人民公园正门\"]"
101 */ 96 */
  97 + // TODO @scholar:CollectionUtils.convertList 可以把 4 行变成 1 行。
  98 + // TODO @scholar:templateParams 拼写错误哈
102 List<String> templateParas = new ArrayList<>(); 99 List<String> templateParas = new ArrayList<>();
103 for (KeyValue<String, Object> kv : templateParams) { 100 for (KeyValue<String, Object> kv : templateParams) {
104 templateParas.add(String.valueOf(kv.getValue())); 101 templateParas.add(String.valueOf(kv.getValue()));
105 } 102 }
106 103
107 - //请求Body,不携带签名名称时,signature请填null  
108 - String body = buildRequestBody(sender, receiver, templateId, templateParas, statusCallBack, null); 104 + // 请求Body,不携带签名名称时,signature请填null
  105 + String body = buildRequestBody(sender, mobile, templateId, templateParas, statusCallBack, null);
  106 + // TODO @scholar:Assert 断言,抛出异常
109 if (null == body || body.isEmpty()) { 107 if (null == body || body.isEmpty()) {
110 return null; 108 return null;
111 } 109 }
@@ -114,6 +112,7 @@ public class HuaweiSmsClient extends AbstractSmsClient { @@ -114,6 +112,7 @@ public class HuaweiSmsClient extends AbstractSmsClient {
114 + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody; 112 + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
115 113
116 // ************* 步骤 2:拼接待签名字符串 ************* 114 // ************* 步骤 2:拼接待签名字符串 *************
  115 + // TODO @scholar:sha256Hex 是不是更简洁哈
117 String hashedCanonicalRequest = HexUtil.encodeHexStr(DigestUtil.sha256(canonicalRequest)); 116 String hashedCanonicalRequest = HexUtil.encodeHexStr(DigestUtil.sha256(canonicalRequest));
118 String stringToSign = "SDK-HMAC-SHA256" + "\n" + singerDate + "\n" + hashedCanonicalRequest; 117 String stringToSign = "SDK-HMAC-SHA256" + "\n" + singerDate + "\n" + hashedCanonicalRequest;
119 118
@@ -125,22 +124,26 @@ public class HuaweiSmsClient extends AbstractSmsClient { @@ -125,22 +124,26 @@ public class HuaweiSmsClient extends AbstractSmsClient {
125 + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature; 124 + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature;
126 125
127 // ************* 步骤 5:构造HttpRequest 并执行request请求,获得response ************* 126 // ************* 步骤 5:构造HttpRequest 并执行request请求,获得response *************
  127 + // TODO @scholar:考虑了下,还是换 hutool 的 httpUtils。因为未来 httpclient 我们可能会移除掉
128 HttpUriRequest postMethod = RequestBuilder.post() 128 HttpUriRequest postMethod = RequestBuilder.post()
129 .setUri(url) 129 .setUri(url)
130 .setEntity(new StringEntity(body, StandardCharsets.UTF_8)) 130 .setEntity(new StringEntity(body, StandardCharsets.UTF_8))
131 .setHeader("Content-Type","application/x-www-form-urlencoded") 131 .setHeader("Content-Type","application/x-www-form-urlencoded")
132 - .setHeader("X-Sdk-Date",singerDate)  
133 - .setHeader("Authorization",authorization) 132 + .setHeader("X-Sdk-Date", singerDate)
  133 + .setHeader("Authorization", authorization)
134 .build(); 134 .build();
  135 + // TODO @scholar:这种不太适合一直 new 的哈
135 CloseableHttpClient client = HttpClientBuilder.create().build(); 136 CloseableHttpClient client = HttpClientBuilder.create().build();
136 HttpResponse response = client.execute(postMethod); 137 HttpResponse response = client.execute(postMethod);
137 - 138 + // TODO @scholar:失败的情况下的处理
  139 + // TODO @scholar:setSerialNo(Integer.toString(response.getStatusLine().getStatusCode())) 这部分,空一行。一行代码太多了,阅读性不太好哈
138 return new SmsSendRespDTO().setSuccess(Objects.equals(response.getStatusLine().getReasonPhrase(), API_CODE_SUCCESS)).setSerialNo(Integer.toString(response.getStatusLine().getStatusCode())) 140 return new SmsSendRespDTO().setSuccess(Objects.equals(response.getStatusLine().getReasonPhrase(), API_CODE_SUCCESS)).setSerialNo(Integer.toString(response.getStatusLine().getStatusCode()))
139 .setApiRequestId(null).setApiCode(null).setApiMsg(null); 141 .setApiRequestId(null).setApiCode(null).setApiMsg(null);
140 } 142 }
141 143
142 static String buildRequestBody(String sender, String receiver, String templateId, List<String> templateParas, 144 static String buildRequestBody(String sender, String receiver, String templateId, List<String> templateParas,
143 - String statusCallBack, String signature) throws UnsupportedEncodingException { 145 + String statusCallBack, @SuppressWarnings("SameParameterValue") String signature) {
  146 + // TODO @scholar:参数不满足,是不是抛出异常更好哈;通过 hutool 的 Assert 去断言
144 if (null == sender || null == receiver || null == templateId || sender.isEmpty() || receiver.isEmpty() 147 if (null == sender || null == receiver || null == templateId || sender.isEmpty() || receiver.isEmpty()
145 || templateId.isEmpty()) { 148 || templateId.isEmpty()) {
146 System.out.println("buildRequestBody(): sender, receiver or templateId is null."); 149 System.out.println("buildRequestBody(): sender, receiver or templateId is null.");
@@ -151,17 +154,20 @@ public class HuaweiSmsClient extends AbstractSmsClient { @@ -151,17 +154,20 @@ public class HuaweiSmsClient extends AbstractSmsClient {
151 appendToBody(body, "from=", sender); 154 appendToBody(body, "from=", sender);
152 appendToBody(body, "&to=", receiver); 155 appendToBody(body, "&to=", receiver);
153 appendToBody(body, "&templateId=", templateId); 156 appendToBody(body, "&templateId=", templateId);
  157 + // TODO @scholar:new JSONArray(templateParas).toString(),是不是 JsonUtils.toString 呀?
154 appendToBody(body, "&templateParas=", new JSONArray(templateParas).toString()); 158 appendToBody(body, "&templateParas=", new JSONArray(templateParas).toString());
155 appendToBody(body, "&statusCallback=", statusCallBack); 159 appendToBody(body, "&statusCallback=", statusCallBack);
156 appendToBody(body, "&signature=", signature); 160 appendToBody(body, "&signature=", signature);
157 return body.toString(); 161 return body.toString();
158 } 162 }
159 163
160 - private static void appendToBody(StringBuilder body, String key, String val) throws UnsupportedEncodingException { 164 + private static void appendToBody(StringBuilder body, String key, String val) {
  165 + // TODO @scholar:StrUtils.isNotEmpty(val),是不是更简洁哈
161 if (null != val && !val.isEmpty()) { 166 if (null != val && !val.isEmpty()) {
162 - body.append(key).append(URLEncoder.encode(val, "UTF-8")); 167 + body.append(key).append(URLEncoder.encode(val, StandardCharsets.UTF_8));
163 } 168 }
164 } 169 }
  170 +
165 @Override 171 @Override
166 public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) { 172 public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
167 List<SmsReceiveStatus> statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class); 173 List<SmsReceiveStatus> statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class);
@@ -173,10 +179,10 @@ public class HuaweiSmsClient extends AbstractSmsClient { @@ -173,10 +179,10 @@ public class HuaweiSmsClient extends AbstractSmsClient {
173 179
174 @Override 180 @Override
175 public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable { 181 public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
176 - //华为短信模板查询和发送短信,是不同的两套key和secret,与阿里、腾讯的区别较大,这里模板查询校验暂不实现。  
177 - return new SmsTemplateRespDTO().setId(null).setContent(null) 182 + // 华为短信模板查询和发送短信,是不同的两套 key 和 secret,与阿里、腾讯的区别较大,这里模板查询校验暂不实现
  183 + // 对应文档 https://support.huaweicloud.com/api-msgsms/sms_05_0040.html
  184 + return new SmsTemplateRespDTO().setId(apiTemplateId).setContent(null)
178 .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(null); 185 .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(null);
179 -  
180 } 186 }
181 187
182 /** 188 /**
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/utils/HuaweiRequest.java deleted
1 -package cn.iocoder.yudao.module.system.framework.sms.core.client.utils;  
2 -  
3 -  
4 -import java.io.UnsupportedEncodingException;  
5 -import java.net.URLDecoder;  
6 -import java.net.URLEncoder;  
7 -import java.util.ArrayList;  
8 -import java.util.Hashtable;  
9 -import java.util.Iterator;  
10 -import java.util.List;  
11 -import java.util.Map;  
12 -import java.util.regex.Matcher;  
13 -import java.util.regex.Pattern;  
14 -  
15 -/**  
16 - * 华为短信待签名request  
17 - *  
18 - * @author scholar  
19 - * @since 2024/6/02 11:55  
20 - */  
21 -public class HuaweiRequest {  
22 - private String key = null;  
23 - private String secret = null;  
24 - private String method = null;  
25 - private String url = null;  
26 - private String body = null;  
27 - private String fragment = null;  
28 - private Map<String, String> headers = new Hashtable();  
29 - private Map<String, List<String>> queryString = new Hashtable();  
30 - private static final Pattern PATTERN = Pattern.compile("^(?i)(post|put|patch|delete|get|options|head)$");  
31 -  
32 - public HuaweiRequest() {  
33 - }  
34 -  
35 - /** @deprecated */  
36 - @Deprecated  
37 - public String getRegion() {  
38 - return "";  
39 - }  
40 -  
41 - /** @deprecated */  
42 - @Deprecated  
43 - public String getServiceName() {  
44 - return "";  
45 - }  
46 -  
47 - public String getKey() {  
48 - return this.key;  
49 - }  
50 -  
51 - public String getSecrect() {  
52 - return this.secret;  
53 - }  
54 -  
55 -// public HttpMethodName getMethod() {  
56 -// return HttpMethodName.valueOf(this.method.toUpperCase(Locale.getDefault()));  
57 -// }  
58 -  
59 - public String getBody() {  
60 - return this.body;  
61 - }  
62 -  
63 - public Map<String, String> getHeaders() {  
64 - return this.headers;  
65 - }  
66 -  
67 - /** @deprecated */  
68 - @Deprecated  
69 - public void setRegion(String region) {  
70 - }  
71 -  
72 - /** @deprecated */  
73 - @Deprecated  
74 - public void setServiceName(String serviceName) {  
75 - }  
76 -  
77 - public void setAppKey(String appKey) throws RuntimeException {  
78 - if (null != appKey && !appKey.trim().isEmpty()) {  
79 - this.key = appKey;  
80 - } else {  
81 - throw new RuntimeException("appKey can not be empty");  
82 - }  
83 - }  
84 -  
85 - public void setAppSecrect(String appSecret) throws RuntimeException {  
86 - if (null != appSecret && !appSecret.trim().isEmpty()) {  
87 - this.secret = appSecret;  
88 - } else {  
89 - throw new RuntimeException("appSecrect can not be empty");  
90 - }  
91 - }  
92 -  
93 - public void setKey(String appKey) throws RuntimeException {  
94 - if (null != appKey && !appKey.trim().isEmpty()) {  
95 - this.key = appKey;  
96 - } else {  
97 - throw new RuntimeException("appKey can not be empty");  
98 - }  
99 - }  
100 -  
101 - public void setSecret(String appSecret) throws RuntimeException {  
102 - if (null != appSecret && !appSecret.trim().isEmpty()) {  
103 - this.secret = appSecret;  
104 - } else {  
105 - throw new RuntimeException("appSecrect can not be empty");  
106 - }  
107 - }  
108 -  
109 - public void setMethod(String method) throws RuntimeException {  
110 - if (null == method) {  
111 - throw new RuntimeException("method can not be empty");  
112 - } else {  
113 - Matcher match = PATTERN.matcher(method);  
114 - if (!match.matches()) {  
115 - throw new RuntimeException("unsupported method");  
116 - } else {  
117 - this.method = method;  
118 - }  
119 - }  
120 - }  
121 -  
122 - public String getUrl() throws UnsupportedEncodingException {  
123 - StringBuilder uri = new StringBuilder();  
124 - uri.append(this.url);  
125 - if (this.queryString.size() > 0) {  
126 - uri.append("?");  
127 - int loop = 0;  
128 - Iterator var3 = this.queryString.entrySet().iterator();  
129 -  
130 - while(var3.hasNext()) {  
131 - Map.Entry<String, List<String>> entry = (Map.Entry)var3.next();  
132 -  
133 - for(Iterator var5 = ((List)entry.getValue()).iterator(); var5.hasNext(); ++loop) {  
134 - String value = (String)var5.next();  
135 - if (loop > 0) {  
136 - uri.append("&");  
137 - }  
138 -  
139 - uri.append(URLEncoder.encode((String)entry.getKey(), "UTF-8"));  
140 - uri.append("=");  
141 - uri.append(URLEncoder.encode(value, "UTF-8"));  
142 - }  
143 - }  
144 - }  
145 -  
146 - if (this.fragment != null) {  
147 - uri.append("#");  
148 - uri.append(this.fragment);  
149 - }  
150 -  
151 - return uri.toString();  
152 - }  
153 -  
154 - public void setUrl(String urlRet) throws RuntimeException, UnsupportedEncodingException {  
155 - if (urlRet != null && !urlRet.trim().isEmpty()) {  
156 - int i = urlRet.indexOf(35);  
157 - if (i >= 0) {  
158 - urlRet = urlRet.substring(0, i);  
159 - }  
160 -  
161 - i = urlRet.indexOf(63);  
162 - this.url = urlRet;  
163 - if (i >= 0) {  
164 - String query = urlRet.substring(i + 1, urlRet.length());  
165 - String[] var4 = query.split("&");  
166 - int var5 = var4.length;  
167 -  
168 - for(int var6 = 0; var6 < var5; ++var6) {  
169 - String item = var4[var6];  
170 - String[] spl = item.split("=", 2);  
171 - String keyRet = spl[0];  
172 - String value = "";  
173 - if (spl.length > 1) {  
174 - value = spl[1];  
175 - }  
176 -  
177 - if (!keyRet.trim().isEmpty()) {  
178 - keyRet = URLDecoder.decode(keyRet, "UTF-8");  
179 - value = URLDecoder.decode(value, "UTF-8");  
180 - this.addQueryStringParam(keyRet, value);  
181 - }  
182 - }  
183 -  
184 - urlRet = urlRet.substring(0, i);  
185 - this.url = urlRet;  
186 - }  
187 - } else {  
188 - throw new RuntimeException("url can not be empty");  
189 - }  
190 - }  
191 -  
192 - public String getPath() {  
193 - String urlRet = this.url;  
194 - int i = urlRet.indexOf("://");  
195 - if (i >= 0) {  
196 - urlRet = urlRet.substring(i + 3);  
197 - }  
198 -  
199 - i = urlRet.indexOf(47);  
200 - return i >= 0 ? urlRet.substring(i) : "/";  
201 - }  
202 -  
203 - public String getHost() {  
204 - String urlRet = this.url;  
205 - int i = urlRet.indexOf("://");  
206 - if (i >= 0) {  
207 - urlRet = urlRet.substring(i + 3);  
208 - }  
209 -  
210 - i = urlRet.indexOf(47);  
211 - if (i >= 0) {  
212 - urlRet = urlRet.substring(0, i);  
213 - }  
214 -  
215 - return urlRet;  
216 - }  
217 -  
218 - public void setBody(String body) {  
219 - this.body = body;  
220 - }  
221 -  
222 - public void addQueryStringParam(String name, String value) {  
223 - List<String> paramList = (List)this.queryString.get(name);  
224 - if (paramList == null) {  
225 - paramList = new ArrayList();  
226 - this.queryString.put(name, paramList);  
227 - }  
228 -  
229 - ((List)paramList).add(value);  
230 - }  
231 -  
232 - public Map<String, List<String>> getQueryStringParams() {  
233 - return this.queryString;  
234 - }  
235 -  
236 - public String getFragment() {  
237 - return this.fragment;  
238 - }  
239 -  
240 - public void setFragment(String fragment) throws RuntimeException, UnsupportedEncodingException {  
241 - if (fragment != null && !fragment.trim().isEmpty()) {  
242 - this.fragment = URLEncoder.encode(fragment, "UTF-8");  
243 - } else {  
244 - throw new RuntimeException("fragment can not be empty");  
245 - }  
246 - }  
247 -  
248 - public void addHeader(String name, String value) {  
249 - if (name != null && !name.trim().isEmpty()) {  
250 - this.headers.put(name, value);  
251 - }  
252 - }  
253 -}  
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/utils/HuaweiSigner.java deleted
1 -package cn.iocoder.yudao.module.system.framework.sms.core.client.utils;  
2 -  
3 -  
4 -import cn.hutool.core.util.HexUtil;  
5 -import cn.hutool.core.util.URLUtil;  
6 -import java.io.UnsupportedEncodingException;  
7 -import java.net.URI;  
8 -import java.net.URISyntaxException;  
9 -import java.nio.charset.StandardCharsets;  
10 -import java.security.InvalidKeyException;  
11 -import java.security.MessageDigest;  
12 -import java.security.NoSuchAlgorithmException;  
13 -import java.security.Security;  
14 -import java.text.SimpleDateFormat;  
15 -import java.util.*;  
16 -import java.util.regex.Matcher;  
17 -import java.util.regex.Pattern;  
18 -import javax.crypto.Mac;  
19 -import javax.crypto.spec.SecretKeySpec;  
20 -import org.apache.commons.codec.binary.StringUtils;  
21 -import org.bouncycastle.crypto.digests.SM3Digest;  
22 -import org.openeuler.BGMJCEProvider;  
23 -  
24 -/**  
25 - * 华为短信request签名实现类  
26 - *  
27 - * @author scholar  
28 - * @since 2024/6/02 11:55  
29 - */  
30 -public class HuaweiSigner {  
31 - public static final String LINE_SEPARATOR = "\n";  
32 - public static final String SDK_SIGNING_ALGORITHM = "SDK-HMAC-SHA256";  
33 - public static final String X_SDK_CONTENT_SHA256 = "x-sdk-content-sha256";  
34 - public static final String X_SDK_DATE = "X-Sdk-Date";  
35 - public static final String AUTHORIZATION = "Authorization";  
36 - private static final Pattern AUTHORIZATION_PATTERN_SHA256 = Pattern.compile("SDK-HMAC-SHA256\\s+Access=([^,]+),\\s?SignedHeaders=([^,]+),\\s?Signature=(\\w+)");  
37 - private static final Pattern AUTHORIZATION_PATTERN_SM3 = Pattern.compile("SDK-HMAC-SM3\\s+Access=([^,]+),\\s?SignedHeaders=([^,]+),\\s?Signature=(\\w+)");  
38 - private static final String LINUX_NEW_LINE = "\n";  
39 - public static final String HOST = "Host";  
40 - public String messageDigestAlgorithm = "SDK-HMAC-SHA256";  
41 -  
42 - public HuaweiSigner(String messageDigestAlgorithm) {  
43 - this.messageDigestAlgorithm = messageDigestAlgorithm;  
44 - }  
45 -  
46 - public HuaweiSigner() {  
47 - }  
48 -  
49 - public void sign(HuaweiRequest request) throws UnsupportedEncodingException {  
50 - String singerDate = this.getHeader(request, "X-Sdk-Date");  
51 - SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.ENGLISH);  
52 - sdf.setTimeZone(TimeZone.getTimeZone("UTC"));  
53 - if (singerDate == null) {  
54 - singerDate = sdf.format(new Date());  
55 - request.addHeader("X-Sdk-Date", singerDate);  
56 - }  
57 -  
58 - this.addHostHeader(request);  
59 - String messageDigestContent = this.calculateContentHash(request);  
60 - String[] signedHeaders = this.getSignedHeaders(request);  
61 - String canonicalRequest = this.createCanonicalRequest(request, signedHeaders, messageDigestContent);  
62 - byte[] signingKey = this.deriveSigningKey(request.getSecrect());  
63 - String stringToSign = this.createStringToSign(canonicalRequest, singerDate);  
64 - byte[] signature = this.computeSignature(stringToSign, signingKey);  
65 - String signatureResult = this.buildAuthorizationHeader(signedHeaders, signature, request.getKey());  
66 - request.addHeader("Authorization", signatureResult);  
67 - }  
68 -  
69 - protected String getCanonicalizedResourcePath(String resourcePath) throws UnsupportedEncodingException {  
70 - if (resourcePath != null && !resourcePath.isEmpty()) {  
71 - try {  
72 - resourcePath = (new URI(resourcePath)).getPath();  
73 - } catch (URISyntaxException var3) {  
74 - return resourcePath;  
75 - }  
76 -  
77 - String value = URLUtil.encode(resourcePath);  
78 - if (!value.startsWith("/")) {  
79 - value = "/".concat(value);  
80 - }  
81 -  
82 - if (!value.endsWith("/")) {  
83 - value = value.concat("/");  
84 - }  
85 -  
86 - return value;  
87 - } else {  
88 - return "/";  
89 - }  
90 - }  
91 -  
92 - protected String getCanonicalizedQueryString(Map<String, List<String>> parameters) throws UnsupportedEncodingException {  
93 - SortedMap<String, List<String>> sorted = new TreeMap();  
94 - Iterator var3 = parameters.entrySet().iterator();  
95 -  
96 - while(var3.hasNext()) {  
97 - Map.Entry<String, List<String>> entry = (Map.Entry)var3.next();  
98 - String encodedParamName = URLUtil.encode((String) entry.getKey());  
99 - List<String> paramValues = (List)entry.getValue();  
100 - List<String> encodedValues = new ArrayList(paramValues.size());  
101 - Iterator var8 = paramValues.iterator();  
102 -  
103 - while(var8.hasNext()) {  
104 - String value = (String)var8.next();  
105 - encodedValues.add(URLUtil.encode(value));  
106 - }  
107 -  
108 - Collections.sort(encodedValues);  
109 - sorted.put(encodedParamName, encodedValues);  
110 - }  
111 -  
112 - StringBuilder result = new StringBuilder();  
113 - Iterator var11 = sorted.entrySet().iterator();  
114 -  
115 - while(var11.hasNext()) {  
116 - Map.Entry<String, List<String>> entry = (Map.Entry)var11.next();  
117 -  
118 - String value;  
119 - for(Iterator var13 = ((List)entry.getValue()).iterator(); var13.hasNext(); result.append((String)entry.getKey()).append("=").append(value)) {  
120 - value = (String)var13.next();  
121 - if (result.length() > 0) {  
122 - result.append("&");  
123 - }  
124 - }  
125 - }  
126 -  
127 - return result.toString();  
128 - }  
129 -  
130 - protected String createCanonicalRequest(HuaweiRequest request, String[] signedHeaders, String messageDigestContent) throws UnsupportedEncodingException {  
131 - return "POST" + "\n" + this.getCanonicalizedResourcePath(request.getPath()) + "\n" + this.getCanonicalizedQueryString(request.getQueryStringParams()) + "\n" + this.getCanonicalizedHeaderString(request, signedHeaders) + "\n" + this.getSignedHeadersString(signedHeaders) + "\n" + messageDigestContent;  
132 - }  
133 -  
134 - protected String createStringToSign(String canonicalRequest, String singerDate) {  
135 - return StringUtils.equals(this.messageDigestAlgorithm, "SDK-HMAC-SHA256") ? this.messageDigestAlgorithm + "\n" + singerDate + "\n" + HexUtil.encodeHexStr(this.hash(canonicalRequest)) : this.messageDigestAlgorithm + "\n" + singerDate + "\n" + HexUtil.encodeHexStr(this.hashSm3(canonicalRequest));  
136 - }  
137 -  
138 - private byte[] deriveSigningKey(String secret) {  
139 - return secret.getBytes(StandardCharsets.UTF_8);  
140 - }  
141 -  
142 - protected byte[] sign(byte[] data, byte[] key, String algorithm) {  
143 - try {  
144 - if (Objects.equals(algorithm, "HmacSM3")) {  
145 - Security.insertProviderAt(new BGMJCEProvider(), 1);  
146 - }  
147 -  
148 - Mac mac = Mac.getInstance(algorithm);  
149 - mac.init(new SecretKeySpec(key, algorithm));  
150 - return mac.doFinal(data);  
151 - } catch (InvalidKeyException | NoSuchAlgorithmException var5) {  
152 - return new byte[0];  
153 - }  
154 - }  
155 -  
156 - protected final byte[] computeSignature(String stringToSign, byte[] signingKey) {  
157 - return StringUtils.equals(this.messageDigestAlgorithm, "SDK-HMAC-SHA256") ? this.sign(stringToSign.getBytes(StandardCharsets.UTF_8), signingKey, "HmacSHA256") : this.sign(stringToSign.getBytes(StandardCharsets.UTF_8), signingKey, "HmacSM3");  
158 - }  
159 -  
160 - private String buildAuthorizationHeader(String[] signedHeaders, byte[] signature, String accessKey) {  
161 - String credential = "Access=" + accessKey;  
162 - String signerHeaders = "SignedHeaders=" + this.getSignedHeadersString(signedHeaders);  
163 - String signatureHeader = "Signature=" + HexUtil.encodeHexStr(signature);  
164 - return this.messageDigestAlgorithm + " " + credential + ", " + signerHeaders + ", " + signatureHeader;  
165 - }  
166 -  
167 - protected String[] getSignedHeaders(HuaweiRequest request) {  
168 - String[] signedHeaders = (String[])request.getHeaders().keySet().toArray(new String[0]);  
169 - Arrays.sort(signedHeaders, String.CASE_INSENSITIVE_ORDER);  
170 - return signedHeaders;  
171 - }  
172 -  
173 - protected String getCanonicalizedHeaderString(HuaweiRequest request, String[] signedHeaders) {  
174 - Map<String, String> requestHeaders = request.getHeaders();  
175 - StringBuilder buffer = new StringBuilder();  
176 - String[] var5 = signedHeaders;  
177 - int var6 = signedHeaders.length;  
178 -  
179 - for(int var7 = 0; var7 < var6; ++var7) {  
180 - String header = var5[var7];  
181 - String key = header.toLowerCase(Locale.getDefault());  
182 - String value = (String)requestHeaders.get(header);  
183 - buffer.append(key).append(":");  
184 - if (value != null) {  
185 - buffer.append(value.trim());  
186 - }  
187 -  
188 - buffer.append("\n");  
189 - }  
190 -  
191 - return buffer.toString();  
192 - }  
193 -  
194 - protected String getSignedHeadersString(String[] signedHeaders) {  
195 - StringBuilder buffer = new StringBuilder();  
196 - String[] var3 = signedHeaders;  
197 - int var4 = signedHeaders.length;  
198 -  
199 - for(int var5 = 0; var5 < var4; ++var5) {  
200 - String header = var3[var5];  
201 - if (buffer.length() > 0) {  
202 - buffer.append(";");  
203 - }  
204 -  
205 - buffer.append(header.toLowerCase(Locale.getDefault()));  
206 - }  
207 -  
208 - return buffer.toString();  
209 - }  
210 -  
211 - protected void addHostHeader(HuaweiRequest request) {  
212 - boolean haveHostHeader = false;  
213 - Iterator var3 = request.getHeaders().keySet().iterator();  
214 -  
215 - while(var3.hasNext()) {  
216 - String key = (String)var3.next();  
217 - if ("Host".equalsIgnoreCase(key)) {  
218 - haveHostHeader = true;  
219 - break;  
220 - }  
221 - }  
222 -  
223 - if (!haveHostHeader) {  
224 - request.addHeader("Host", request.getHost());  
225 - }  
226 -  
227 - }  
228 -  
229 - protected String getHeader(HuaweiRequest request, String header) {  
230 - if (header == null) {  
231 - return null;  
232 - } else {  
233 - Map<String, String> headers = request.getHeaders();  
234 - Iterator var4 = headers.entrySet().iterator();  
235 -  
236 - Map.Entry entry;  
237 - do {  
238 - if (!var4.hasNext()) {  
239 - return null;  
240 - }  
241 -  
242 - entry = (Map.Entry)var4.next();  
243 - } while(!header.equalsIgnoreCase((String)entry.getKey()));  
244 -  
245 - return (String)entry.getValue();  
246 - }  
247 - }  
248 -  
249 - public boolean verify(HuaweiRequest request) throws UnsupportedEncodingException {  
250 - String singerDate = this.getHeader(request, "X-Sdk-Date");  
251 - String authorization = this.getHeader(request, "Authorization");  
252 - Matcher match = AUTHORIZATION_PATTERN_SM3.matcher(authorization);  
253 - if (StringUtils.equals(this.messageDigestAlgorithm, "SDK-HMAC-SHA256")) {  
254 - match = AUTHORIZATION_PATTERN_SHA256.matcher(authorization);  
255 - }  
256 -  
257 - if (!match.find()) {  
258 - return false;  
259 - } else {  
260 - String[] signedHeaders = match.group(2).split(";");  
261 - byte[] signingKey = this.deriveSigningKey(request.getSecrect());  
262 - String messageDigestContent = this.calculateContentHash(request);  
263 - String canonicalRequest = this.createCanonicalRequest(request, signedHeaders, messageDigestContent);  
264 - String stringToSign = this.createStringToSign(canonicalRequest, singerDate);  
265 - byte[] signature = this.computeSignature(stringToSign, signingKey);  
266 - String signatureResult = this.buildAuthorizationHeader(signedHeaders, signature, request.getKey());  
267 - return signatureResult.equals(authorization);  
268 - }  
269 - }  
270 -  
271 - protected String calculateContentHash(HuaweiRequest request) {  
272 - String content_sha256 = this.getHeader(request, "x-sdk-content-sha256");  
273 - if (content_sha256 != null) {  
274 - return content_sha256;  
275 - } else {  
276 - return StringUtils.equals(this.messageDigestAlgorithm, "SDK-HMAC-SHA256") ? HexUtil.encodeHexStr(this.hash(request.getBody()),true) : HexUtil.encodeHexStr(this.hashSm3(request.getBody()));  
277 - }  
278 - }  
279 -  
280 - public byte[] hash(String text) {  
281 - try {  
282 - MessageDigest md = MessageDigest.getInstance("SHA-256");  
283 - md.update(text.getBytes(StandardCharsets.UTF_8));  
284 - return md.digest();  
285 - } catch (NoSuchAlgorithmException var3) {  
286 - return new byte[0];  
287 - }  
288 - }  
289 -  
290 - public byte[] hashSm3(String text) {  
291 - byte[] srcData = text.getBytes(StandardCharsets.UTF_8);  
292 - SM3Digest digest = new SM3Digest();  
293 - digest.update(srcData, 0, srcData.length);  
294 - byte[] hash = new byte[digest.getDigestSize()];  
295 - digest.doFinal(hash, 0);  
296 - return hash;  
297 - }  
298 -  
299 -  
300 -}  
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.KeyValue;
  4 +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
  5 +import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
  6 +import org.junit.jupiter.api.Disabled;
  7 +import org.junit.jupiter.api.Test;
  8 +
  9 +import java.util.List;
  10 +
  11 +/**
  12 + * 各种 {@link SmsClientTests 集成测试
  13 + *
  14 + * @author 芋道源码
  15 + */
  16 +public class SmsClientTests {
  17 +
  18 + @Test
  19 + @Disabled
  20 + public void testHuaweiSmsClient() throws Throwable {
  21 + SmsChannelProperties properties = new SmsChannelProperties()
  22 + .setApiKey("123")
  23 + .setApiSecret("456");
  24 + HuaweiSmsClient client = new HuaweiSmsClient(properties);
  25 + // 准备参数
  26 + Long sendLogId = System.currentTimeMillis();
  27 + String mobile = "15601691323";
  28 + String apiTemplateId = "xx test01";
  29 + List<KeyValue<String, Object>> templateParams = List.of(new KeyValue<>("code", "1024"));
  30 + // 调用
  31 + SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
  32 + // 打印结果
  33 + System.out.println(smsSendRespDTO);
  34 + }
  35 +
  36 +}