diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java index bd603b543..ae3138362 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java @@ -1,7 +1,11 @@ package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; +import cn.hutool.core.date.format.FastDateFormat; import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.HexUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.crypto.digest.HmacAlgorithm; import cn.hutool.json.JSONArray; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; @@ -14,12 +18,8 @@ import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateR import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; import com.google.common.annotations.VisibleForTesting; -import jakarta.xml.bind.DatatypeConverter; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; -import java.text.SimpleDateFormat; import java.util.*; import static cn.hutool.crypto.digest.DigestUtil.sha256Hex; @@ -34,6 +34,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils. */ public class TencentSmsClient extends AbstractSmsClient { + private static final String HOST = "sms.tencentcloudapi.com"; private static final String VERSION = "2021-01-11"; private static final String REGION = "ap-guangzhou"; @@ -89,7 +90,7 @@ public class TencentSmsClient extends AbstractSmsClient { body.put("PhoneNumberSet", new String[]{mobile}); body.put("SmsSdkAppId", getSdkAppId()); body.put("SignName", properties.getSignature()); - body.put("TemplateId",apiTemplateId); + body.put("TemplateId", apiTemplateId); body.put("TemplateParamSet", ArrayUtils.toArray(templateParams, param -> String.valueOf(param.getValue()))); JSONObject response = request("SendSms", body); @@ -102,11 +103,11 @@ public class TencentSmsClient extends AbstractSmsClient { .setApiCode(error.getStr("Code")) .setApiMsg(error.getStr("Message")); } - JSONObject responseData = responseResult.getJSONArray("SendStatusSet").getJSONObject(0); - return new SmsSendRespDTO().setSuccess(Objects.equals(API_CODE_SUCCESS, responseData.getStr("Code"))) + JSONObject sendResult = responseResult.getJSONArray("SendStatusSet").getJSONObject(0); + return new SmsSendRespDTO().setSuccess(Objects.equals(API_CODE_SUCCESS, sendResult.getStr("Code"))) .setApiRequestId(responseResult.getStr("RequestId")) - .setSerialNo(responseData.getStr("SerialNo")) - .setApiMsg(responseData.getStr("Message")); + .setSerialNo(sendResult.getStr("SerialNo")) + .setApiMsg(sendResult.getStr("Message")); } @Override @@ -133,14 +134,13 @@ public class TencentSmsClient extends AbstractSmsClient { body.put("TemplateIdSet", new Integer[]{Integer.valueOf(apiTemplateId)}); JSONObject response = request("DescribeSmsTemplateList", body); - // TODO @scholar:会有请求失败的情况么?类似发送的(那块逻辑我补充了) - JSONObject TemplateStatusSet = response.getJSONObject("Response").getJSONArray("DescribeTemplateStatusSet").getJSONObject(0); - String content = TemplateStatusSet.get("TemplateContent").toString(); - int templateStatus = Integer.parseInt(TemplateStatusSet.get("StatusCode").toString()); - String auditReason = TemplateStatusSet.get("ReviewReply").toString(); - - return new SmsTemplateRespDTO().setId(apiTemplateId).setContent(content) - .setAuditStatus(convertSmsTemplateAuditStatus(templateStatus)).setAuditReason(auditReason); + // 2. 解析请求 + JSONObject statusResult = response.getJSONObject("Response") + .getJSONArray("DescribeTemplateStatusSet").getJSONObject(0); + return new SmsTemplateRespDTO().setId(apiTemplateId) + .setContent(statusResult.get("TemplateContent").toString()) + .setAuditStatus(convertSmsTemplateAuditStatus(statusResult.getInt("StatusCode"))) + .setAuditReason(statusResult.get("ReviewReply").toString()); } @VisibleForTesting @@ -163,63 +163,39 @@ public class TencentSmsClient extends AbstractSmsClient { * @return 请求结果 */ private JSONObject request(String action, TreeMap body) throws Exception { - String timestamp = String.valueOf(System.currentTimeMillis() / 1000); - // TODO @scholar:这个 format,看看怎么写的可以简化点 - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); - // 注意时区,否则容易出错 - sdf.setTimeZone(TimeZone.getTimeZone("UTC")); - String date = sdf.format(new Date(Long.valueOf(timestamp + "000"))); - - // TODO @scholar:这个步骤,看看怎么参考阿里云 client,归类下;1. 2.1 2.2 这种 - // ************* 步骤 1:拼接规范请求串 ************* - // TODO @scholar:这个 hsot 枚举下; - String host = "sms.tencentcloudapi.com"; //APP接入地址+接口访问URI - String httpMethod = "POST"; // 请求方式 - String canonicalUri = "/"; - String canonicalQueryString = ""; - - String canonicalHeaders = "content-type:application/json; charset=utf-8\n" - + "host:" + host + "\n" + "x-tc-action:" + action.toLowerCase() + "\n"; - String signedHeaders = "content-type;host;x-tc-action"; - String hashedRequestBody = sha256Hex(JSONUtil.toJsonStr(body)); - // TODO @scholar:换行下,不然单行太长了 - String canonicalRequest = httpMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody; - - // ************* 步骤 2:拼接待签名字符串 ************* - String credentialScope = date + "/" + "sms" + "/" + "tc3_request"; - String hashedCanonicalRequest = sha256Hex(canonicalRequest); - String stringToSign = "TC3-HMAC-SHA256" + "\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest; - - // ************* 步骤 3:计算签名 ************* - byte[] secretDate = hmac256(("TC3" + properties.getApiSecret()).getBytes(StandardCharsets.UTF_8), date); - byte[] secretService = hmac256(secretDate, "sms"); - byte[] secretSigning = hmac256(secretService, "tc3_request"); - String signature = DatatypeConverter.printHexBinary(hmac256(secretSigning, stringToSign)).toLowerCase(); - - // ************* 步骤 4:拼接 Authorization ************* - String authorization = "TC3-HMAC-SHA256" + " " + "Credential=" + getApiKey() + "/" + credentialScope + ", " - + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature; - - // ************* 步骤 5:构造HttpRequest 并执行request请求,获得response ************* + // 1.1 请求 Header Map headers = new HashMap<>(); - headers.put("Authorization", authorization); headers.put("Content-Type", "application/json; charset=utf-8"); - headers.put("Host", host); + headers.put("Host", HOST); headers.put("X-TC-Action", action); - headers.put("X-TC-Timestamp", timestamp); + Date now = new Date(); + String nowStr = FastDateFormat.getInstance("yyyy-MM-dd", TimeZone.getTimeZone("UTC")).format(now); + headers.put("X-TC-Timestamp", String.valueOf(now.getTime() / 1000)); headers.put("X-TC-Version", VERSION); headers.put("X-TC-Region", REGION); - String responseBody = HttpUtils.post("https://" + host, headers, JSONUtil.toJsonStr(body)); + // 1.2 构建签名 Header + String canonicalQueryString = ""; + String canonicalHeaders = "content-type:application/json; charset=utf-8\n" + + "host:" + HOST + "\n" + "x-tc-action:" + action.toLowerCase() + "\n"; + String signedHeaders = "content-type;host;x-tc-action"; + String canonicalRequest = "POST" + "\n" + "/" + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + + signedHeaders + "\n" + sha256Hex(JSONUtil.toJsonStr(body)); + String credentialScope = nowStr + "/" + "sms" + "/" + "tc3_request"; + String stringToSign = "TC3-HMAC-SHA256" + "\n" + now.getTime() / 1000 + "\n" + credentialScope + "\n" + + sha256Hex(canonicalRequest); + byte[] secretService = hmac256(hmac256(("TC3" + properties.getApiSecret()).getBytes(StandardCharsets.UTF_8), nowStr), "sms"); + String signature = HexUtil.encodeHexStr(hmac256(hmac256(secretService, "tc3_request"), stringToSign)); + headers.put("Authorization", "TC3-HMAC-SHA256" + " " + "Credential=" + getApiKey() + "/" + credentialScope + ", " + + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature); + // 2. 发起请求 + String responseBody = HttpUtils.post("https://" + HOST, headers, JSONUtil.toJsonStr(body)); return JSONUtil.parseObj(responseBody); } - // TODO @scholar:使用 hutool 简化下 - private static byte[] hmac256(byte[] key, String msg) throws Exception { - Mac mac = Mac.getInstance("HmacSHA256"); - SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm()); - mac.init(secretKeySpec); - return mac.doFinal(msg.getBytes(StandardCharsets.UTF_8)); + private static byte[] hmac256(byte[] key, String msg) { + return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, key).digest(msg); } + } \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java index cb6c6c9f4..3105c4369 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java +++ b/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 { @Test @Disabled public void testTencentSmsClient_sendSms() throws Throwable { + String sdkAppId = "1400500458"; SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR 1428926523") - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz") + .setApiKey(System.getenv("SMS_TENCENT_ACCESS_KEY") + " " + sdkAppId) + .setApiSecret(System.getenv("SMS_TENCENT_SECRET_KEY")) .setSignature("芋道源码"); TencentSmsClient client = new TencentSmsClient(properties); // 准备参数 Long sendLogId = System.currentTimeMillis(); String mobile = "15601691323"; - String apiTemplateId = "2136358"; + String apiTemplateId = "358212"; // 调用 SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024"))); // 打印结果 @@ -75,13 +76,14 @@ public class SmsClientTests { @Test @Disabled public void testTencentSmsClient_getSmsTemplate() throws Throwable { + String sdkAppId = "1400500458"; SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR 1428926523") - .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz") + .setApiKey(System.getenv("SMS_TENCENT_ACCESS_KEY") + " " + sdkAppId) + .setApiSecret(System.getenv("SMS_TENCENT_SECRET_KEY")) .setSignature("芋道源码"); TencentSmsClient client = new TencentSmsClient(properties); // 准备参数 - String apiTemplateId = "2136358"; + String apiTemplateId = "358212"; // 调用 SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId); // 打印结果 diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java index b25540b44..060a34558 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java +++ b/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 { } @Test - public void testDoSendSms_fail() throws Throwable { + public void testDoSendSms_fail_01() throws Throwable { try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { // 准备参数 Long sendLogId = randomLongId(); @@ -117,6 +117,31 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { } } + @Test + public void testDoSendSms_fail_02() throws Throwable { + try (MockedStatic httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString(); + List> templateParams = Lists.newArrayList( + new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); + + // mock 方法 + httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) + .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\"}}"); + + // 调用 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, + apiTemplateId, templateParams); + // 断言 + assertFalse(result.getSuccess()); + assertEquals("2a88f82a-261c-4ac6-9fa9-c7d01aaa486a", result.getApiRequestId()); + assertEquals("AuthFailure.SecretIdNotFound", result.getApiCode()); + assertEquals("The SecretId is not found, please ensure that your SecretId is correct.", result.getApiMsg()); + } + } + @Test public void testParseSmsReceiveStatus() { // 准备参数