Commit 8da08072422404b1ee66781102beda41f51701e5
1 parent
af5be1bc
【代码评审】SYSTEM:华为云 client 的 review
Showing
5 changed files
with
76 additions
and
588 deletions
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 | +} | ... | ... |