Commit a5f82fedb3a47bd54f4f34f2c7d923195edd5d01
1 parent
5b7e637e
【功能优化】短信:腾讯云的实现优化
Showing
3 changed files
with
76 additions
and
73 deletions
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" + |