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 46 @PostMapping("/huawei")
47 47 @PermitAll
48 48 @Operation(summary = "华为云短信的回调", description = "参见 https://support.huaweicloud.com/api-msgsms/sms_05_0003.html 文档")
49   - @OperateLog(enable = false)
50 49 public CommonResult<Boolean> receiveHuaweiSmsStatus(HttpServletRequest request) throws Throwable {
51 50 String text = ServletUtils.getBody(request);
52 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 1 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
2 2  
3   -
4 3 import cn.hutool.core.lang.Assert;
5 4 import cn.hutool.core.util.HexUtil;
6 5 import cn.hutool.core.util.StrUtil;
7   -
8 6 import cn.hutool.crypto.SecureUtil;
9 7 import cn.hutool.crypto.digest.DigestUtil;
10 8 import cn.hutool.json.JSONArray;
... ... @@ -15,34 +13,27 @@ import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespD
15 13 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
16 14 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
17 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 16 import com.fasterxml.jackson.annotation.JsonFormat;
26 17 import com.fasterxml.jackson.annotation.JsonProperty;
27 18 import lombok.Data;
28 19 import lombok.extern.slf4j.Slf4j;
29 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 27 import java.net.URLEncoder;
33 28 import java.nio.charset.StandardCharsets;
34 29 import java.text.SimpleDateFormat;
35   -import java.util.*;
36   -
37   -
38 30 import java.time.LocalDateTime;
39   -
  31 +import java.util.*;
40 32  
41 33 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
42 34 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
43 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 47 * 调用成功 code
57 48 */
58 49 public static final String API_CODE_SUCCESS = "OK";
59   - private static final Logger LOGGER = LoggerFactory.getLogger(HuaweiSmsClient.class);
60 50  
61 51 public HuaweiSmsClient(SmsChannelProperties properties) {
62 52 super(properties);
63 53 Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
64 54 Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
65 55 }
  56 +
66 57 @Override
67 58 protected void doInit() {
68   -
69 59 }
  60 +
70 61 @Override
71 62 public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
72 63 List<KeyValue<String, Object>> templateParams) throws Throwable {
  64 + // TODO @scholar:https://smsapi.cn-north-4.myhuaweicloud.com:443 是不是枚举成静态变量
73 65 String url = "https://smsapi.cn-north-4.myhuaweicloud.com:443/sms/batchSendSms/v1"; //APP接入地址+接口访问URI
74 66 // 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构
75 67 // 所以将 通道号 拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"。空格为分隔符。
  68 + // TODO @scholar:暂时只考虑中国大陆,所以不需要 sender 哈
76 69 String sender = StrUtil.subAfter(apiTemplateId, " ", true); //中国大陆短信签名通道号或全球短信通道号
77 70 String templateId = StrUtil.subBefore(apiTemplateId, " ", true); //模板ID
78 71  
79   - //必填,全局号码格式(包含国家码),示例:+86151****6789,多个号码之间用英文逗号分隔
80   - String receiver = mobile; //短信接收人号码
81   -
82   - //选填,短信状态报告接收地址,推荐使用域名,为空或者不填表示不接收状态报告
  72 + // 选填,短信状态报告接收地址,推荐使用域名,为空或者不填表示不接收状态报告
83 73 String statusCallBack = properties.getCallbackUrl();
84 74  
  75 + // TODO @scholar:1)是不是用 LocalDateTimeUtil.format();这样 3 行变成一行
  76 + // TODO @scholar:singerDate 叫 sdkDate 会更合适哈,这样理解起来简单。另外,singer 应该是 signed 么?
85 77 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.ENGLISH);
86 78 sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
87 79 String singerDate = sdf.format(new Date());
88 80  
  81 + // TODO @scholar:整个处理加密的过程,是不是应该抽成一个 private 方法哈。这样整个调用的主干更清晰。
89 82 // ************* 步骤 1:拼接规范请求串 *************
90 83 String httpRequestMethod = "POST";
91 84 String canonicalUri = "/sms/batchSendSms/v1/";
92   - String canonicalQueryString = "";//查询参数为空
  85 + String canonicalQueryString = ""; // 查询参数为空
93 86 String canonicalHeaders = "content-type:application/x-www-form-urlencoded\n"
94 87 + "host:smsapi.cn-north-4.myhuaweicloud.com:443\n"
95 88 + "x-sdk-date:" + singerDate + "\n";
  89 + // TODO @scholar:静态枚举了
96 90 String signedHeaders = "content-type;host;x-sdk-date";
97   - /**
  91 + // TODO @scholar:下面的注释,可以考虑去掉
  92 + /*
98 93 * 选填,使用无变量模板时请赋空值 String templateParas = "";
99 94 * 单变量模板示例:模板内容为"您的验证码是${NUM_6}"时,templateParas可填写为"[\"111111\"]"
100 95 * 双变量模板示例:模板内容为"您有${NUM_2}件快递请到${TXT_20}领取"时,templateParas可填写为"[\"3\",\"人民公园正门\"]"
101 96 */
  97 + // TODO @scholar:CollectionUtils.convertList 可以把 4 行变成 1 行。
  98 + // TODO @scholar:templateParams 拼写错误哈
102 99 List<String> templateParas = new ArrayList<>();
103 100 for (KeyValue<String, Object> kv : templateParams) {
104 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 107 if (null == body || body.isEmpty()) {
110 108 return null;
111 109 }
... ... @@ -114,6 +112,7 @@ public class HuaweiSmsClient extends AbstractSmsClient {
114 112 + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
115 113  
116 114 // ************* 步骤 2:拼接待签名字符串 *************
  115 + // TODO @scholar:sha256Hex 是不是更简洁哈
117 116 String hashedCanonicalRequest = HexUtil.encodeHexStr(DigestUtil.sha256(canonicalRequest));
118 117 String stringToSign = "SDK-HMAC-SHA256" + "\n" + singerDate + "\n" + hashedCanonicalRequest;
119 118  
... ... @@ -125,22 +124,26 @@ public class HuaweiSmsClient extends AbstractSmsClient {
125 124 + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature;
126 125  
127 126 // ************* 步骤 5:构造HttpRequest 并执行request请求,获得response *************
  127 + // TODO @scholar:考虑了下,还是换 hutool 的 httpUtils。因为未来 httpclient 我们可能会移除掉
128 128 HttpUriRequest postMethod = RequestBuilder.post()
129 129 .setUri(url)
130 130 .setEntity(new StringEntity(body, StandardCharsets.UTF_8))
131 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 134 .build();
  135 + // TODO @scholar:这种不太适合一直 new 的哈
135 136 CloseableHttpClient client = HttpClientBuilder.create().build();
136 137 HttpResponse response = client.execute(postMethod);
137   -
  138 + // TODO @scholar:失败的情况下的处理
  139 + // TODO @scholar:setSerialNo(Integer.toString(response.getStatusLine().getStatusCode())) 这部分,空一行。一行代码太多了,阅读性不太好哈
138 140 return new SmsSendRespDTO().setSuccess(Objects.equals(response.getStatusLine().getReasonPhrase(), API_CODE_SUCCESS)).setSerialNo(Integer.toString(response.getStatusLine().getStatusCode()))
139 141 .setApiRequestId(null).setApiCode(null).setApiMsg(null);
140 142 }
141 143  
142 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 147 if (null == sender || null == receiver || null == templateId || sender.isEmpty() || receiver.isEmpty()
145 148 || templateId.isEmpty()) {
146 149 System.out.println("buildRequestBody(): sender, receiver or templateId is null.");
... ... @@ -151,17 +154,20 @@ public class HuaweiSmsClient extends AbstractSmsClient {
151 154 appendToBody(body, "from=", sender);
152 155 appendToBody(body, "&to=", receiver);
153 156 appendToBody(body, "&templateId=", templateId);
  157 + // TODO @scholar:new JSONArray(templateParas).toString(),是不是 JsonUtils.toString 呀?
154 158 appendToBody(body, "&templateParas=", new JSONArray(templateParas).toString());
155 159 appendToBody(body, "&statusCallback=", statusCallBack);
156 160 appendToBody(body, "&signature=", signature);
157 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 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 171 @Override
166 172 public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
167 173 List<SmsReceiveStatus> statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class);
... ... @@ -173,10 +179,10 @@ public class HuaweiSmsClient extends AbstractSmsClient {
173 179  
174 180 @Override
175 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 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 +}
... ...