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 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 +import cn.hutool.core.date.format.FastDateFormat;
3 import cn.hutool.core.lang.Assert; 4 import cn.hutool.core.lang.Assert;
  5 +import cn.hutool.core.util.HexUtil;
4 import cn.hutool.core.util.StrUtil; 6 import cn.hutool.core.util.StrUtil;
  7 +import cn.hutool.crypto.digest.DigestUtil;
  8 +import cn.hutool.crypto.digest.HmacAlgorithm;
5 import cn.hutool.json.JSONArray; 9 import cn.hutool.json.JSONArray;
6 import cn.hutool.json.JSONObject; 10 import cn.hutool.json.JSONObject;
7 import cn.hutool.json.JSONUtil; 11 import cn.hutool.json.JSONUtil;
@@ -14,12 +18,8 @@ import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateR @@ -14,12 +18,8 @@ import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateR
14 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; 18 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
15 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; 19 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
16 import com.google.common.annotations.VisibleForTesting; 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 import java.nio.charset.StandardCharsets; 22 import java.nio.charset.StandardCharsets;
22 -import java.text.SimpleDateFormat;  
23 import java.util.*; 23 import java.util.*;
24 24
25 import static cn.hutool.crypto.digest.DigestUtil.sha256Hex; 25 import static cn.hutool.crypto.digest.DigestUtil.sha256Hex;
@@ -34,6 +34,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils. @@ -34,6 +34,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
34 */ 34 */
35 public class TencentSmsClient extends AbstractSmsClient { 35 public class TencentSmsClient extends AbstractSmsClient {
36 36
  37 + private static final String HOST = "sms.tencentcloudapi.com";
37 private static final String VERSION = "2021-01-11"; 38 private static final String VERSION = "2021-01-11";
38 private static final String REGION = "ap-guangzhou"; 39 private static final String REGION = "ap-guangzhou";
39 40
@@ -89,7 +90,7 @@ public class TencentSmsClient extends AbstractSmsClient { @@ -89,7 +90,7 @@ public class TencentSmsClient extends AbstractSmsClient {
89 body.put("PhoneNumberSet", new String[]{mobile}); 90 body.put("PhoneNumberSet", new String[]{mobile});
90 body.put("SmsSdkAppId", getSdkAppId()); 91 body.put("SmsSdkAppId", getSdkAppId());
91 body.put("SignName", properties.getSignature()); 92 body.put("SignName", properties.getSignature());
92 - body.put("TemplateId",apiTemplateId); 93 + body.put("TemplateId", apiTemplateId);
93 body.put("TemplateParamSet", ArrayUtils.toArray(templateParams, param -> String.valueOf(param.getValue()))); 94 body.put("TemplateParamSet", ArrayUtils.toArray(templateParams, param -> String.valueOf(param.getValue())));
94 JSONObject response = request("SendSms", body); 95 JSONObject response = request("SendSms", body);
95 96
@@ -102,11 +103,11 @@ public class TencentSmsClient extends AbstractSmsClient { @@ -102,11 +103,11 @@ public class TencentSmsClient extends AbstractSmsClient {
102 .setApiCode(error.getStr("Code")) 103 .setApiCode(error.getStr("Code"))
103 .setApiMsg(error.getStr("Message")); 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 .setApiRequestId(responseResult.getStr("RequestId")) 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 @Override 113 @Override
@@ -133,14 +134,13 @@ public class TencentSmsClient extends AbstractSmsClient { @@ -133,14 +134,13 @@ public class TencentSmsClient extends AbstractSmsClient {
133 body.put("TemplateIdSet", new Integer[]{Integer.valueOf(apiTemplateId)}); 134 body.put("TemplateIdSet", new Integer[]{Integer.valueOf(apiTemplateId)});
134 JSONObject response = request("DescribeSmsTemplateList", body); 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 @VisibleForTesting 146 @VisibleForTesting
@@ -163,63 +163,39 @@ public class TencentSmsClient extends AbstractSmsClient { @@ -163,63 +163,39 @@ public class TencentSmsClient extends AbstractSmsClient {
163 * @return 请求结果 163 * @return 请求结果
164 */ 164 */
165 private JSONObject request(String action, TreeMap<String, Object> body) throws Exception { 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 Map<String, String> headers = new HashMap<>(); 167 Map<String, String> headers = new HashMap<>();
205 - headers.put("Authorization", authorization);  
206 headers.put("Content-Type", "application/json; charset=utf-8"); 168 headers.put("Content-Type", "application/json; charset=utf-8");
207 - headers.put("Host", host); 169 + headers.put("Host", HOST);
208 headers.put("X-TC-Action", action); 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 headers.put("X-TC-Version", VERSION); 174 headers.put("X-TC-Version", VERSION);
211 headers.put("X-TC-Region", REGION); 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 return JSONUtil.parseObj(responseBody); 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 \ No newline at end of file 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,15 +57,16 @@ public class SmsClientTests {
57 @Test 57 @Test
58 @Disabled 58 @Disabled
59 public void testTencentSmsClient_sendSms() throws Throwable { 59 public void testTencentSmsClient_sendSms() throws Throwable {
  60 + String sdkAppId = "1400500458";
60 SmsChannelProperties properties = new SmsChannelProperties() 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 .setSignature("芋道源码"); 64 .setSignature("芋道源码");
64 TencentSmsClient client = new TencentSmsClient(properties); 65 TencentSmsClient client = new TencentSmsClient(properties);
65 // 准备参数 66 // 准备参数
66 Long sendLogId = System.currentTimeMillis(); 67 Long sendLogId = System.currentTimeMillis();
67 String mobile = "15601691323"; 68 String mobile = "15601691323";
68 - String apiTemplateId = "2136358"; 69 + String apiTemplateId = "358212";
69 // 调用 70 // 调用
70 SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024"))); 71 SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024")));
71 // 打印结果 72 // 打印结果
@@ -75,13 +76,14 @@ public class SmsClientTests { @@ -75,13 +76,14 @@ public class SmsClientTests {
75 @Test 76 @Test
76 @Disabled 77 @Disabled
77 public void testTencentSmsClient_getSmsTemplate() throws Throwable { 78 public void testTencentSmsClient_getSmsTemplate() throws Throwable {
  79 + String sdkAppId = "1400500458";
78 SmsChannelProperties properties = new SmsChannelProperties() 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 .setSignature("芋道源码"); 83 .setSignature("芋道源码");
82 TencentSmsClient client = new TencentSmsClient(properties); 84 TencentSmsClient client = new TencentSmsClient(properties);
83 // 准备参数 85 // 准备参数
84 - String apiTemplateId = "2136358"; 86 + String apiTemplateId = "358212";
85 // 调用 87 // 调用
86 SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId); 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,7 +78,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
78 } 78 }
79 79
80 @Test 80 @Test
81 - public void testDoSendSms_fail() throws Throwable { 81 + public void testDoSendSms_fail_01() throws Throwable {
82 try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { 82 try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
83 // 准备参数 83 // 准备参数
84 Long sendLogId = randomLongId(); 84 Long sendLogId = randomLongId();
@@ -118,6 +118,31 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { @@ -118,6 +118,31 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
118 } 118 }
119 119
120 @Test 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 public void testParseSmsReceiveStatus() { 146 public void testParseSmsReceiveStatus() {
122 // 准备参数 147 // 准备参数
123 String text = "[\n" + 148 String text = "[\n" +