Commit 1c1abae5bbf8357fb32044e766cb92b06603dcfe
1 parent
4f7ac969
七牛云短信实现,评审意见修改
Showing
4 changed files
with
51 additions
and
63 deletions
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java
| ... | ... | @@ -135,7 +135,6 @@ public class HttpUtils { |
| 135 | 135 | * @return 请求结果 |
| 136 | 136 | */ |
| 137 | 137 | public static String post(String url, Map<String, String> headers, String requestBody) { |
| 138 | - | |
| 139 | 138 | try (HttpResponse response = HttpRequest.post(url) |
| 140 | 139 | .addHeaders(headers) |
| 141 | 140 | .body(requestBody) |
| ... | ... | @@ -154,7 +153,6 @@ public class HttpUtils { |
| 154 | 153 | * @return 请求结果 |
| 155 | 154 | */ |
| 156 | 155 | public static String get(String url, Map<String, String> headers) { |
| 157 | - | |
| 158 | 156 | try (HttpResponse response = HttpRequest.get(url) |
| 159 | 157 | .addHeaders(headers) |
| 160 | 158 | .execute()) { | ... | ... |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClient.java
| 1 | 1 | package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.collection.CollStreamUtil; | |
| 3 | 4 | import cn.hutool.core.collection.ListUtil; |
| 5 | +import cn.hutool.core.date.DateUtil; | |
| 4 | 6 | import cn.hutool.core.date.LocalDateTimeUtil; |
| 5 | 7 | import cn.hutool.core.lang.Assert; |
| 8 | +import cn.hutool.core.util.ObjUtil; | |
| 6 | 9 | import cn.hutool.core.util.ObjectUtil; |
| 7 | -import cn.hutool.crypto.digest.HMac; | |
| 10 | +import cn.hutool.crypto.SecureUtil; | |
| 8 | 11 | import cn.hutool.crypto.digest.HmacAlgorithm; |
| 9 | 12 | import cn.hutool.json.JSONObject; |
| 10 | 13 | import cn.hutool.json.JSONUtil; |
| ... | ... | @@ -18,11 +21,7 @@ import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProp |
| 18 | 21 | import com.google.common.annotations.VisibleForTesting; |
| 19 | 22 | import lombok.extern.slf4j.Slf4j; |
| 20 | 23 | |
| 21 | -import java.nio.charset.StandardCharsets; | |
| 22 | -import java.text.SimpleDateFormat; | |
| 23 | - | |
| 24 | 24 | import java.util.*; |
| 25 | -import java.util.stream.Collectors; | |
| 26 | 25 | |
| 27 | 26 | /** |
| 28 | 27 | * 七牛云短信客户端的实现类 |
| ... | ... | @@ -45,69 +44,60 @@ public class QiniuSmsClient extends AbstractSmsClient { |
| 45 | 44 | Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); |
| 46 | 45 | } |
| 47 | 46 | |
| 48 | - @Override | |
| 49 | 47 | protected void doInit() { |
| 50 | 48 | } |
| 51 | - @Override | |
| 49 | + | |
| 52 | 50 | public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, |
| 53 | 51 | List<KeyValue<String, Object>> templateParams) throws Throwable { |
| 54 | - | |
| 55 | 52 | // 1. 执行请求 |
| 56 | 53 | // 参考链接 https://developer.qiniu.com/sms/5824/through-the-api-send-text-messages |
| 57 | 54 | LinkedHashMap<String, Object> body = new LinkedHashMap<>(); |
| 58 | - Map<String, Object> paramsMap = templateParams.stream() | |
| 59 | - .collect(Collectors.toMap(KeyValue::getKey, KeyValue::getValue)); | |
| 60 | - | |
| 61 | 55 | body.put("template_id", apiTemplateId); |
| 62 | 56 | body.put("mobile", mobile); |
| 63 | - body.put("parameters", paramsMap); | |
| 57 | + body.put("parameters", CollStreamUtil.toMap(templateParams, KeyValue::getKey, KeyValue::getValue)); | |
| 64 | 58 | body.put("seq", Long.toString(sendLogId)); |
| 65 | 59 | |
| 66 | - JSONObject response = request("POST", body, null); | |
| 60 | + JSONObject response = request("POST", body, PATH); | |
| 67 | 61 | // 2. 解析请求 |
| 62 | + if (ObjectUtil.isNotEmpty(response.getStr("error"))){//短信请求失败 | |
| 63 | + return new SmsSendRespDTO().setSuccess(false) | |
| 64 | + .setApiCode(response.getStr("error")) | |
| 65 | + .setApiRequestId(response.getStr("request_id")) | |
| 66 | + .setApiMsg(response.getStr("message")); | |
| 67 | + } | |
| 68 | + | |
| 68 | 69 | return new SmsSendRespDTO().setSuccess(response.containsKey("message_id")) |
| 69 | 70 | .setSerialNo(response.getStr("message_id")); |
| 70 | 71 | } |
| 71 | 72 | |
| 72 | - | |
| 73 | 73 | /** |
| 74 | 74 | * 请求七牛云短信 |
| 75 | 75 | * |
| 76 | 76 | * @see <a href="https://developer.qiniu.com/sms/5842/sms-api-authentication"</> |
| 77 | 77 | * @param httpMethod http请求方法 |
| 78 | - * @param queryParams 请求参数 | |
| 78 | + * @param body http请求消息体 | |
| 79 | + * @param path URL path | |
| 79 | 80 | * @return 请求结果 |
| 80 | 81 | */ |
| 81 | - private JSONObject request(String httpMethod, LinkedHashMap<String, Object> body, Map<String, Object> queryParams) { | |
| 82 | - | |
| 83 | - String signature = ""; | |
| 84 | - String templateIdPath = ""; | |
| 85 | - | |
| 86 | - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); | |
| 87 | - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); | |
| 88 | - String signDate = dateFormat.format(new Date()); | |
| 89 | - | |
| 82 | + private JSONObject request(String httpMethod, LinkedHashMap<String, Object> body, String path) { | |
| 83 | + String signDate = DateUtil.date().setTimeZone(TimeZone.getTimeZone("UTC")).toString("yyyyMMdd'T'HHmmss'Z'"); | |
| 90 | 84 | //请求头 |
| 91 | 85 | Map<String, String> header = new HashMap<>(4); |
| 92 | 86 | header.put("HOST", HOST); |
| 93 | - header.put("Authorization", signature); | |
| 87 | + header.put("Authorization", getSignature(httpMethod, HOST, path, body != null ? JSONUtil.toJsonStr(body) : "", signDate)); | |
| 94 | 88 | header.put("Content-Type", "application/json"); |
| 95 | 89 | header.put("X-Qiniu-Date", signDate); |
| 96 | 90 | |
| 97 | 91 | String responseBody =""; |
| 98 | - if(Objects.equals(httpMethod, "POST")){ | |
| 99 | - header.put("Authorization", getSignature(httpMethod, HOST, PATH, JSONUtil.toJsonStr(body), signDate)); | |
| 100 | - responseBody = HttpUtils.post("https://" + HOST + PATH, header, JSONUtil.toJsonStr(body)); | |
| 101 | - }else { // GET | |
| 102 | - templateIdPath = TEMPLATE_PATH + "/" + queryParams.get("template_id"); | |
| 103 | - header.put("Authorization", getSignature(httpMethod, HOST, templateIdPath, null, signDate)); | |
| 104 | - responseBody = HttpUtils.get("https://" + HOST + templateIdPath, header); | |
| 92 | + if (Objects.equals(httpMethod, "POST")){// POST 发送短消息用POST请求 | |
| 93 | + responseBody = HttpUtils.post("https://" + HOST + path, header, JSONUtil.toJsonStr(body)); | |
| 94 | + }else { // GET 查询template状态用GET请求 | |
| 95 | + responseBody = HttpUtils.get("https://" + HOST + path, header); | |
| 105 | 96 | } |
| 106 | 97 | return JSONUtil.parseObj(responseBody); |
| 107 | 98 | } |
| 108 | 99 | |
| 109 | 100 | public String getSignature(String method, String host, String path, String body, String signDate) { |
| 110 | - | |
| 111 | 101 | StringBuilder dataToSign = new StringBuilder(); |
| 112 | 102 | dataToSign.append(method.toUpperCase()).append(" ").append(path); |
| 113 | 103 | dataToSign.append("\nHost: ").append(host); |
| ... | ... | @@ -117,18 +107,15 @@ public class QiniuSmsClient extends AbstractSmsClient { |
| 117 | 107 | if (ObjectUtil.isNotEmpty(body)) { |
| 118 | 108 | dataToSign.append(body); |
| 119 | 109 | } |
| 120 | - HMac hMac = new HMac(HmacAlgorithm.HmacSHA1, properties.getApiSecret().getBytes(StandardCharsets.UTF_8)); | |
| 121 | - byte[] signData = hMac.digest(dataToSign.toString().getBytes(StandardCharsets.UTF_8)); | |
| 122 | - String encodedSignature = Base64.getEncoder().encodeToString(signData); | |
| 110 | + String encodedSignature = SecureUtil.hmac(HmacAlgorithm.HmacSHA1, properties.getApiSecret()).digestBase64(dataToSign.toString(), true); | |
| 123 | 111 | |
| 124 | 112 | return "Qiniu " + properties.getApiKey() + ":" + encodedSignature; |
| 125 | 113 | } |
| 126 | 114 | |
| 127 | 115 | @Override |
| 128 | 116 | public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) { |
| 129 | - | |
| 130 | 117 | JSONObject status = JSONUtil.parseObj(text); |
| 131 | - //字段参考 https://developer.qiniu.com/sms/5910/message-push | |
| 118 | + // 字段参考 https://developer.qiniu.com/sms/5910/message-push | |
| 132 | 119 | return ListUtil.of(new SmsReceiveRespDTO() |
| 133 | 120 | .setSuccess("DELIVRD".equals(status.getJSONArray("items").getJSONObject(0).getStr("status"))) // 是否接收成功 |
| 134 | 121 | .setErrorMsg(status.getJSONArray("items").getJSONObject(0).getStr("status")) |
| ... | ... | @@ -142,16 +129,13 @@ public class QiniuSmsClient extends AbstractSmsClient { |
| 142 | 129 | public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable { |
| 143 | 130 | // 1. 执行请求 |
| 144 | 131 | // 参考链接 https://developer.qiniu.com/sms/5969/query-a-single-template |
| 145 | - HashMap<String, Object> queryParam = new HashMap<>(); | |
| 146 | - queryParam.put("template_id", apiTemplateId); | |
| 147 | - JSONObject response = request("GET", null, queryParam); | |
| 148 | - | |
| 132 | + JSONObject response = request("GET", null, TEMPLATE_PATH + "/" + apiTemplateId); | |
| 149 | 133 | // 2.1 请求失败 |
| 150 | - String status = response.getStr("audit_status"); | |
| 151 | - if (!Objects.equals(status, "passed")) { | |
| 134 | + if (ObjUtil.notEqual(response.getStr("audit_status"), "passed")) { | |
| 152 | 135 | log.error("[getSmsTemplate][模版编号({}) 响应不正确({})]", apiTemplateId, response); |
| 153 | 136 | return null; |
| 154 | 137 | } |
| 138 | + | |
| 155 | 139 | // 2.2 请求成功 |
| 156 | 140 | return new SmsTemplateRespDTO() |
| 157 | 141 | .setId(response.getStr("id")) |
| ... | ... | @@ -162,11 +146,12 @@ public class QiniuSmsClient extends AbstractSmsClient { |
| 162 | 146 | |
| 163 | 147 | @VisibleForTesting |
| 164 | 148 | Integer convertSmsTemplateAuditStatus(String templateStatus) { |
| 165 | - | |
| 166 | - if(Objects.equals(templateStatus, "passed")){ | |
| 167 | - return SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); | |
| 168 | - }else { | |
| 169 | - throw new IllegalArgumentException(String.format("未知审核状态(%str)", templateStatus)); | |
| 170 | - } | |
| 149 | + return switch (templateStatus) { | |
| 150 | + case "passed" -> SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); | |
| 151 | + case "reviewing" -> SmsTemplateAuditStatusEnum.CHECKING.getStatus(); | |
| 152 | + case "rejected" -> SmsTemplateAuditStatusEnum.FAIL.getStatus(); | |
| 153 | + case null, default -> | |
| 154 | + throw new IllegalArgumentException(String.format("未知审核状态(%str)", templateStatus)); | |
| 155 | + }; | |
| 171 | 156 | } |
| 172 | 157 | } | ... | ... |
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/QiniuSmsClientTest.java
| ... | ... | @@ -29,7 +29,6 @@ import static org.mockito.Mockito.mockStatic; |
| 29 | 29 | * @author scholar |
| 30 | 30 | */ |
| 31 | 31 | public class QiniuSmsClientTest extends BaseMockitoUnitTest { |
| 32 | - | |
| 33 | 32 | private final SmsChannelProperties properties = new SmsChannelProperties() |
| 34 | 33 | .setApiKey(randomString())// 随机一个 apiKey,避免构建报错 |
| 35 | 34 | .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 |
| ... | ... | @@ -46,7 +45,6 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest { |
| 46 | 45 | |
| 47 | 46 | @Test |
| 48 | 47 | public void testDoSendSms_success() throws Throwable { |
| 49 | - | |
| 50 | 48 | try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { |
| 51 | 49 | // 准备参数 |
| 52 | 50 | Long sendLogId = randomLongId(); |
| ... | ... | @@ -56,9 +54,7 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest { |
| 56 | 54 | new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); |
| 57 | 55 | // mock 方法 |
| 58 | 56 | httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) |
| 59 | - .thenReturn( | |
| 60 | - "{\"message_id\":\"17245678901\"}" | |
| 61 | - ); | |
| 57 | + .thenReturn("{\"message_id\":\"17245678901\"}"); | |
| 62 | 58 | // 调用 |
| 63 | 59 | SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, |
| 64 | 60 | apiTemplateId, templateParams); |
| ... | ... | @@ -77,17 +73,17 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest { |
| 77 | 73 | String apiTemplateId = randomString() + " " + randomString(); |
| 78 | 74 | List<KeyValue<String, Object>> templateParams = Lists.newArrayList( |
| 79 | 75 | new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); |
| 80 | - | |
| 81 | 76 | // mock 方法 |
| 82 | 77 | httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) |
| 83 | - .thenReturn( | |
| 84 | - "{\"error\":\"BadToken\",\"message\":\"Your authorization token is invalid\",\"request_id\":\"etziWcJFo1C8Ne8X\"}" | |
| 85 | - ); | |
| 78 | + .thenReturn("{\"error\":\"BadToken\",\"message\":\"Your authorization token is invalid\",\"request_id\":\"etziWcJFo1C8Ne8X\"}"); | |
| 86 | 79 | // 调用 |
| 87 | 80 | SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, |
| 88 | 81 | apiTemplateId, templateParams); |
| 89 | 82 | // 断言 |
| 90 | 83 | assertFalse(result.getSuccess()); |
| 84 | + assertEquals("BadToken", result.getApiCode()); | |
| 85 | + assertEquals("Your authorization token is invalid", result.getApiMsg()); | |
| 86 | + assertEquals("etziWcJFo1C8Ne8X", result.getApiRequestId()); | |
| 91 | 87 | } |
| 92 | 88 | } |
| 93 | 89 | |
| ... | ... | @@ -125,4 +121,15 @@ public class QiniuSmsClientTest extends BaseMockitoUnitTest { |
| 125 | 121 | assertEquals(123, statuses.getFirst().getLogId()); |
| 126 | 122 | } |
| 127 | 123 | |
| 124 | + @Test | |
| 125 | + public void testConvertSmsTemplateAuditStatus() { | |
| 126 | + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), | |
| 127 | + smsClient.convertSmsTemplateAuditStatus("passed")); | |
| 128 | + assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(), | |
| 129 | + smsClient.convertSmsTemplateAuditStatus("reviewing")); | |
| 130 | + assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(), | |
| 131 | + smsClient.convertSmsTemplateAuditStatus("rejected")); | |
| 132 | + assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus("unknown"), | |
| 133 | + "未知审核状态(3)"); | |
| 134 | + } | |
| 128 | 135 | } |
| 129 | 136 | \ 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
| ... | ... | @@ -116,7 +116,6 @@ public class SmsClientTests { |
| 116 | 116 | @Test |
| 117 | 117 | @Disabled |
| 118 | 118 | public void testQiniuSmsClient_sendSms() throws Throwable { |
| 119 | - | |
| 120 | 119 | SmsChannelProperties properties = new SmsChannelProperties() |
| 121 | 120 | .setApiKey("SMS_QINIU_ACCESS_KEY") |
| 122 | 121 | .setApiSecret("SMS_QINIU_SECRET_KEY"); |
| ... | ... | @@ -135,7 +134,6 @@ public class SmsClientTests { |
| 135 | 134 | @Test |
| 136 | 135 | @Disabled |
| 137 | 136 | public void testQiniuSmsClient_getSmsTemplate() throws Throwable { |
| 138 | - | |
| 139 | 137 | SmsChannelProperties properties = new SmsChannelProperties() |
| 140 | 138 | .setApiKey("SMS_QINIU_ACCESS_KEY") |
| 141 | 139 | .setApiSecret("SMS_QINIU_SECRET_KEY"); | ... | ... |