Commit a5f82fedb3a47bd54f4f34f2c7d923195edd5d01

Authored by YunaiV
1 parent 5b7e637e

【功能优化】短信:腾讯云的实现优化

yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java
1 1 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
2 2  
  3 +import cn.hutool.core.date.format.FastDateFormat;
3 4 import cn.hutool.core.lang.Assert;
  5 +import cn.hutool.core.util.HexUtil;
4 6 import cn.hutool.core.util.StrUtil;
  7 +import cn.hutool.crypto.digest.DigestUtil;
  8 +import cn.hutool.crypto.digest.HmacAlgorithm;
5 9 import cn.hutool.json.JSONArray;
6 10 import cn.hutool.json.JSONObject;
7 11 import cn.hutool.json.JSONUtil;
... ... @@ -14,12 +18,8 @@ import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateR
14 18 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
15 19 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
16 20 import com.google.common.annotations.VisibleForTesting;
17   -import jakarta.xml.bind.DatatypeConverter;
18 21  
19   -import javax.crypto.Mac;
20   -import javax.crypto.spec.SecretKeySpec;
21 22 import java.nio.charset.StandardCharsets;
22   -import java.text.SimpleDateFormat;
23 23 import java.util.*;
24 24  
25 25 import static cn.hutool.crypto.digest.DigestUtil.sha256Hex;
... ... @@ -34,6 +34,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
34 34 */
35 35 public class TencentSmsClient extends AbstractSmsClient {
36 36  
  37 + private static final String HOST = "sms.tencentcloudapi.com";
37 38 private static final String VERSION = "2021-01-11";
38 39 private static final String REGION = "ap-guangzhou";
39 40  
... ... @@ -89,7 +90,7 @@ public class TencentSmsClient extends AbstractSmsClient {
89 90 body.put("PhoneNumberSet", new String[]{mobile});
90 91 body.put("SmsSdkAppId", getSdkAppId());
91 92 body.put("SignName", properties.getSignature());
92   - body.put("TemplateId",apiTemplateId);
  93 + body.put("TemplateId", apiTemplateId);
93 94 body.put("TemplateParamSet", ArrayUtils.toArray(templateParams, param -> String.valueOf(param.getValue())));
94 95 JSONObject response = request("SendSms", body);
95 96  
... ... @@ -102,11 +103,11 @@ public class TencentSmsClient extends AbstractSmsClient {
102 103 .setApiCode(error.getStr("Code"))
103 104 .setApiMsg(error.getStr("Message"));
104 105 }
105   - JSONObject responseData = responseResult.getJSONArray("SendStatusSet").getJSONObject(0);
106   - return new SmsSendRespDTO().setSuccess(Objects.equals(API_CODE_SUCCESS, responseData.getStr("Code")))
  106 + JSONObject sendResult = responseResult.getJSONArray("SendStatusSet").getJSONObject(0);
  107 + return new SmsSendRespDTO().setSuccess(Objects.equals(API_CODE_SUCCESS, sendResult.getStr("Code")))
107 108 .setApiRequestId(responseResult.getStr("RequestId"))
108   - .setSerialNo(responseData.getStr("SerialNo"))
109   - .setApiMsg(responseData.getStr("Message"));
  109 + .setSerialNo(sendResult.getStr("SerialNo"))
  110 + .setApiMsg(sendResult.getStr("Message"));
110 111 }
111 112  
112 113 @Override
... ... @@ -133,14 +134,13 @@ public class TencentSmsClient extends AbstractSmsClient {
133 134 body.put("TemplateIdSet", new Integer[]{Integer.valueOf(apiTemplateId)});
134 135 JSONObject response = request("DescribeSmsTemplateList", body);
135 136  
136   - // TODO @scholar:会有请求失败的情况么?类似发送的(那块逻辑我补充了)
137   - JSONObject TemplateStatusSet = response.getJSONObject("Response").getJSONArray("DescribeTemplateStatusSet").getJSONObject(0);
138   - String content = TemplateStatusSet.get("TemplateContent").toString();
139   - int templateStatus = Integer.parseInt(TemplateStatusSet.get("StatusCode").toString());
140   - String auditReason = TemplateStatusSet.get("ReviewReply").toString();
141   -
142   - return new SmsTemplateRespDTO().setId(apiTemplateId).setContent(content)
143   - .setAuditStatus(convertSmsTemplateAuditStatus(templateStatus)).setAuditReason(auditReason);
  137 + // 2. 解析请求
  138 + JSONObject statusResult = response.getJSONObject("Response")
  139 + .getJSONArray("DescribeTemplateStatusSet").getJSONObject(0);
  140 + return new SmsTemplateRespDTO().setId(apiTemplateId)
  141 + .setContent(statusResult.get("TemplateContent").toString())
  142 + .setAuditStatus(convertSmsTemplateAuditStatus(statusResult.getInt("StatusCode")))
  143 + .setAuditReason(statusResult.get("ReviewReply").toString());
144 144 }
145 145  
146 146 @VisibleForTesting
... ... @@ -163,63 +163,39 @@ public class TencentSmsClient extends AbstractSmsClient {
163 163 * @return 请求结果
164 164 */
165 165 private JSONObject request(String action, TreeMap<String, Object> body) throws Exception {
166   - String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
167   - // TODO @scholar:这个 format,看看怎么写的可以简化点
168   - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
169   - // 注意时区,否则容易出错
170   - sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
171   - String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
172   -
173   - // TODO @scholar:这个步骤,看看怎么参考阿里云 client,归类下;1. 2.1 2.2 这种
174   - // ************* 步骤 1:拼接规范请求串 *************
175   - // TODO @scholar:这个 hsot 枚举下;
176   - String host = "sms.tencentcloudapi.com"; //APP接入地址+接口访问URI
177   - String httpMethod = "POST"; // 请求方式
178   - String canonicalUri = "/";
179   - String canonicalQueryString = "";
180   -
181   - String canonicalHeaders = "content-type:application/json; charset=utf-8\n"
182   - + "host:" + host + "\n" + "x-tc-action:" + action.toLowerCase() + "\n";
183   - String signedHeaders = "content-type;host;x-tc-action";
184   - String hashedRequestBody = sha256Hex(JSONUtil.toJsonStr(body));
185   - // TODO @scholar:换行下,不然单行太长了
186   - String canonicalRequest = httpMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
187   -
188   - // ************* 步骤 2:拼接待签名字符串 *************
189   - String credentialScope = date + "/" + "sms" + "/" + "tc3_request";
190   - String hashedCanonicalRequest = sha256Hex(canonicalRequest);
191   - String stringToSign = "TC3-HMAC-SHA256" + "\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest;
192   -
193   - // ************* 步骤 3:计算签名 *************
194   - byte[] secretDate = hmac256(("TC3" + properties.getApiSecret()).getBytes(StandardCharsets.UTF_8), date);
195   - byte[] secretService = hmac256(secretDate, "sms");
196   - byte[] secretSigning = hmac256(secretService, "tc3_request");
197   - String signature = DatatypeConverter.printHexBinary(hmac256(secretSigning, stringToSign)).toLowerCase();
198   -
199   - // ************* 步骤 4:拼接 Authorization *************
200   - String authorization = "TC3-HMAC-SHA256" + " " + "Credential=" + getApiKey() + "/" + credentialScope + ", "
201   - + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature;
202   -
203   - // ************* 步骤 5:构造HttpRequest 并执行request请求,获得response *************
  166 + // 1.1 请求 Header
204 167 Map<String, String> headers = new HashMap<>();
205   - headers.put("Authorization", authorization);
206 168 headers.put("Content-Type", "application/json; charset=utf-8");
207   - headers.put("Host", host);
  169 + headers.put("Host", HOST);
208 170 headers.put("X-TC-Action", action);
209   - headers.put("X-TC-Timestamp", timestamp);
  171 + Date now = new Date();
  172 + String nowStr = FastDateFormat.getInstance("yyyy-MM-dd", TimeZone.getTimeZone("UTC")).format(now);
  173 + headers.put("X-TC-Timestamp", String.valueOf(now.getTime() / 1000));
210 174 headers.put("X-TC-Version", VERSION);
211 175 headers.put("X-TC-Region", REGION);
212 176  
213   - String responseBody = HttpUtils.post("https://" + host, headers, JSONUtil.toJsonStr(body));
214   -
  177 + // 1.2 构建签名 Header
  178 + String canonicalQueryString = "";
  179 + String canonicalHeaders = "content-type:application/json; charset=utf-8\n"
  180 + + "host:" + HOST + "\n" + "x-tc-action:" + action.toLowerCase() + "\n";
  181 + String signedHeaders = "content-type;host;x-tc-action";
  182 + String canonicalRequest = "POST" + "\n" + "/" + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n"
  183 + + signedHeaders + "\n" + sha256Hex(JSONUtil.toJsonStr(body));
  184 + String credentialScope = nowStr + "/" + "sms" + "/" + "tc3_request";
  185 + String stringToSign = "TC3-HMAC-SHA256" + "\n" + now.getTime() / 1000 + "\n" + credentialScope + "\n" +
  186 + sha256Hex(canonicalRequest);
  187 + byte[] secretService = hmac256(hmac256(("TC3" + properties.getApiSecret()).getBytes(StandardCharsets.UTF_8), nowStr), "sms");
  188 + String signature = HexUtil.encodeHexStr(hmac256(hmac256(secretService, "tc3_request"), stringToSign));
  189 + headers.put("Authorization", "TC3-HMAC-SHA256" + " " + "Credential=" + getApiKey() + "/" + credentialScope + ", "
  190 + + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature);
  191 +
  192 + // 2. 发起请求
  193 + String responseBody = HttpUtils.post("https://" + HOST, headers, JSONUtil.toJsonStr(body));
215 194 return JSONUtil.parseObj(responseBody);
216 195 }
217 196  
218   - // TODO @scholar:使用 hutool 简化下
219   - private static byte[] hmac256(byte[] key, String msg) throws Exception {
220   - Mac mac = Mac.getInstance("HmacSHA256");
221   - SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
222   - mac.init(secretKeySpec);
223   - return mac.doFinal(msg.getBytes(StandardCharsets.UTF_8));
  197 + private static byte[] hmac256(byte[] key, String msg) {
  198 + return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, key).digest(msg);
224 199 }
  200 +
225 201 }
226 202 \ No newline at end of file
... ...
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java
... ... @@ -57,15 +57,16 @@ public class SmsClientTests {
57 57 @Test
58 58 @Disabled
59 59 public void testTencentSmsClient_sendSms() throws Throwable {
  60 + String sdkAppId = "1400500458";
60 61 SmsChannelProperties properties = new SmsChannelProperties()
61   - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR 1428926523")
62   - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz")
  62 + .setApiKey(System.getenv("SMS_TENCENT_ACCESS_KEY") + " " + sdkAppId)
  63 + .setApiSecret(System.getenv("SMS_TENCENT_SECRET_KEY"))
63 64 .setSignature("芋道源码");
64 65 TencentSmsClient client = new TencentSmsClient(properties);
65 66 // 准备参数
66 67 Long sendLogId = System.currentTimeMillis();
67 68 String mobile = "15601691323";
68   - String apiTemplateId = "2136358";
  69 + String apiTemplateId = "358212";
69 70 // 调用
70 71 SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024")));
71 72 // 打印结果
... ... @@ -75,13 +76,14 @@ public class SmsClientTests {
75 76 @Test
76 77 @Disabled
77 78 public void testTencentSmsClient_getSmsTemplate() throws Throwable {
  79 + String sdkAppId = "1400500458";
78 80 SmsChannelProperties properties = new SmsChannelProperties()
79   - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR 1428926523")
80   - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz")
  81 + .setApiKey(System.getenv("SMS_TENCENT_ACCESS_KEY") + " " + sdkAppId)
  82 + .setApiSecret(System.getenv("SMS_TENCENT_SECRET_KEY"))
81 83 .setSignature("芋道源码");
82 84 TencentSmsClient client = new TencentSmsClient(properties);
83 85 // 准备参数
84   - String apiTemplateId = "2136358";
  86 + String apiTemplateId = "358212";
85 87 // 调用
86 88 SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId);
87 89 // 打印结果
... ...
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java
... ... @@ -78,7 +78,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
78 78 }
79 79  
80 80 @Test
81   - public void testDoSendSms_fail() throws Throwable {
  81 + public void testDoSendSms_fail_01() throws Throwable {
82 82 try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
83 83 // 准备参数
84 84 Long sendLogId = randomLongId();
... ... @@ -118,6 +118,31 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
118 118 }
119 119  
120 120 @Test
  121 + public void testDoSendSms_fail_02() throws Throwable {
  122 + try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
  123 + // 准备参数
  124 + Long sendLogId = randomLongId();
  125 + String mobile = randomString();
  126 + String apiTemplateId = randomString();
  127 + List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
  128 + new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
  129 +
  130 + // mock 方法
  131 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
  132 + .thenReturn("{\"Response\":{\"Error\":{\"Code\":\"AuthFailure.SecretIdNotFound\",\"Message\":\"The SecretId is not found, please ensure that your SecretId is correct.\"},\"RequestId\":\"2a88f82a-261c-4ac6-9fa9-c7d01aaa486a\"}}");
  133 +
  134 + // 调用
  135 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
  136 + apiTemplateId, templateParams);
  137 + // 断言
  138 + assertFalse(result.getSuccess());
  139 + assertEquals("2a88f82a-261c-4ac6-9fa9-c7d01aaa486a", result.getApiRequestId());
  140 + assertEquals("AuthFailure.SecretIdNotFound", result.getApiCode());
  141 + assertEquals("The SecretId is not found, please ensure that your SecretId is correct.", result.getApiMsg());
  142 + }
  143 + }
  144 +
  145 + @Test
121 146 public void testParseSmsReceiveStatus() {
122 147 // 准备参数
123 148 String text = "[\n" +
... ...