oying-system/src/main/java/com/oying/modules/security/rest/VerificationController.java
@@ -4,8 +4,8 @@ import com.oying.annotation.rest.AnonymousGetMapping; import com.oying.utils.R; import com.oying.utils.RedisUtils; import com.oying.utils.SendMessageUtils; import com.oying.utils.WinnerLookProperties; import com.oying.modules.security.utils.SendMessageUtils; import com.oying.modules.security.utils.WinnerLookProperties; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; oying-system/src/main/java/com/oying/modules/security/utils/SendMessageUtils.java
File was renamed from oying-tools/src/main/java/com/oying/utils/SendMessageUtils.java @@ -1,6 +1,8 @@ package com.oying.utils; package com.oying.modules.security.utils; import cn.hutool.core.util.IdUtil; import com.oying.exception.BadRequestException; import com.oying.utils.HttpRequest; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpMethod; @@ -24,8 +26,26 @@ @Data public class SendMessageUtils { public static final String SIGN = "【立研】"; public static final String SIGN = "【哦应科技】"; public static final String MESSAGE = "您的验证码为:{code},请勿泄露于他人!"; public static void main(String[] args) { System.out.println(IdUtil.simpleUUID()); Map<String, String> params = new HashMap<>(); params.put("userCode", "SHOYYJ"); params.put("userPass", "Oy250928"); params.put("DesNo", "15213186640"); params.put("Msg", "【哦应科技】您的验证码为:784121,请勿泄露于他人!"); params.put("smsType", "101"); String str = extractWithRegex(HttpRequest.exchangeMsg(HttpMethod.POST, "https://118.178.116.15:8443/winnerrxd/api/trigger/SendMsg", convert(params))); if (ObjectUtils.isEmpty(str)) { log.error("短信调用异常 {}", str); throw new BadRequestException("短信调用异常"); } long i = Long.parseLong(str); if (i < 0) { throw new BadRequestException(WinnerLookEnum.find(str)); } } public static void sendMsg(String url, String phone, String sign, String message, WinnerLookProperties properties) { Map<String, String> params = new HashMap<>(); oying-system/src/main/java/com/oying/modules/security/utils/WinnerLookEnum.java
File was renamed from oying-tools/src/main/java/com/oying/utils/WinnerLookEnum.java @@ -1,4 +1,4 @@ package com.oying.utils; package com.oying.modules.security.utils; import lombok.AllArgsConstructor; import lombok.Getter; oying-system/src/main/java/com/oying/modules/security/utils/WinnerLookProperties.java
File was renamed from oying-tools/src/main/java/com/oying/utils/WinnerLookProperties.java @@ -1,4 +1,4 @@ package com.oying.utils; package com.oying.modules.security.utils; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -22,6 +22,7 @@ * 每次不要超过50个号码 */ private String urlSendMsgBatch; private String token; private String userCode; private String userPass; } oying-system/src/main/java/com/oying/modules/winnerlook/client/BaseApi.java
New file @@ -0,0 +1,48 @@ package com.oying.modules.winnerlook.client; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import java.util.HashMap; import java.util.Map; /** * @author MinaWu * @description * @date 2025/5/14 16:41 */ @Slf4j public class BaseApi { private static final String API_V1_TRIGGER_SEND_MSG = "/api/v1/trigger/SendMsg"; private static final String API_V1_TRIGGER_SEND_BATCH_MSG = "/api/v1/trigger/SendBatchMsg"; public static String TRIGGER = "101"; /** * 发送短信 * * @param userCode 登录名称 * @param token token * @param desNo 手机号 * @param msg 内容 * @param autograph 签名编号 * @param customerUuid 用户标识字段 * @return String */ public static String sendMsgByPost(String userCode, String token, String desNo, String msg, String autograph, String customerUuid, String baseUrl) { Map<String, Object> params = new HashMap<>(); params.put("userCode", userCode); params.put("DesNo", desNo); params.put("Msg", msg); params.put("smsType", TRIGGER); if (StringUtils.isNotBlank(autograph)) { params.put("autograph", autograph); } if (StringUtils.isNotBlank(customerUuid)) { params.put("customerUuid", customerUuid); } String xmlResponse = OkHttpService.doPost(params, baseUrl, userCode, token); return OkHttpService.parseXmlResponse(xmlResponse); } } oying-system/src/main/java/com/oying/modules/winnerlook/client/OkHttpService.java
New file @@ -0,0 +1,201 @@ package com.oying.modules.winnerlook.client; import cn.hutool.core.util.IdUtil; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.oying.exception.BadRequestException; import com.oying.modules.winnerlook.util.Md5Util; import lombok.extern.slf4j.Slf4j; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import shade.okhttp3.*; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.UUID; /** * @author MinaWu * @description * @date 2025/5/14 16:16 */ @Slf4j public class OkHttpService { private static final String SECRET_KEY = "X-Winner-Secret-Key"; private static final String TIMESTAMP_HEADER = "X-Winner-Timestamp"; private static final String SIGNATURE_HEADER = "X-Winner-Signature"; private static final String REQUEST_ID = "X-Winner-Request-Id"; private static final String TOKEN = "X-Winner-token"; private static final String PARAMS = "params"; /** * 生成 requestId * * @param timestamp 时间戳 * @return requestId */ private static String generateRequestId(String userName, long timestamp) { return userName.replace("_", "") + "_" + IdUtil.simpleUUID() + "_" + timestamp; } public static String doPost(Map<String, Object> params, String baseUrl, String userName, String token) { String secretKey = UUID.randomUUID().toString().replace("-", ""); String boundary = UUID.randomUUID().toString().replace("-", ""); // 生成时间戳 long timestamp = System.currentTimeMillis(); String timestampStr = String.valueOf(timestamp); // 生成 requestId String requestId = generateRequestId(userName, timestamp); // 生成签名 String signature = generateSignature(params, timestampStr, secretKey, requestId, token); // 创建请求体 MultipartBody.Builder bodyBuilder = new MultipartBody.Builder(boundary) .setType(MultipartBody.FORM); for (Map.Entry<String, Object> entry : params.entrySet()) { bodyBuilder.addFormDataPart(entry.getKey(), entry.getValue().toString()); } // 创建请求 Request request = new Request.Builder() .url(baseUrl) .header(TIMESTAMP_HEADER, timestampStr) .header(SIGNATURE_HEADER, signature) .header(REQUEST_ID, requestId) .header(SECRET_KEY, secretKey) .header("Content-Type", "multipart/form-data;charset=UTF-8; boundary=" + boundary) .post(bodyBuilder.build()) .build(); // 发送请求 try { return "response"; } catch (Exception e) { log.info("doPost requestId:{},body:{},headers:{} ", requestId, request.body().toString(), request.headers().toString()); throw new BadRequestException("短信POST:" + e.getMessage()); } } /** * 对 treeMap 的键进行排序 */ private static void sortParams(Map<String, Object> treeMap) { for (String key : treeMap.keySet()) { Object value = treeMap.get(key); if (value instanceof JSONObject) { // 如果是 JSONObject,递归排序 treeMap.put(key, sortJsonObject((JSONObject) value)); } else if (value instanceof JSONArray) { // 如果是 JSONArray,对数组中的每个对象排序 treeMap.put(key, sortJsonArray((JSONArray) value)); } else { // 其他类型直接放入 treeMap.put(key, value); } } } /** * 对 JSONObject 的键进行排序 */ private static JSONObject sortJsonObject(JSONObject jsonObject) { Map<String, Object> treeMap = new TreeMap<>(); Set<String> keys = jsonObject.keySet(); for (String key : keys) { Object value = jsonObject.get(key); if (value instanceof JSONObject) { // 如果是 JSONObject,递归排序 treeMap.put(key, sortJsonObject((JSONObject) value)); } else if (value instanceof JSONArray) { // 如果是 JSONArray,对数组中的每个对象排序 treeMap.put(key, sortJsonArray((JSONArray) value)); } else { // 其他类型直接放入 treeMap.put(key, value); } } return new JSONObject(treeMap); } /** * 对 JSONArray 中的每个 JSONObject 进行排序 */ private static JSONArray sortJsonArray(JSONArray jsonArray) { JSONArray sortedJsonArray = new JSONArray(); for (Object item : jsonArray) { if (item instanceof JSONObject) { // 如果是 JSONObject,递归排序 sortedJsonArray.add(sortJsonObject((JSONObject) item)); } else { // 其他类型直接放入 sortedJsonArray.add(item); } } return sortedJsonArray; } /** * 生成签名 * * @param map 请求参数 * @param timestamp 时间戳 * @param secretKey 密钥 * @return 签名 */ private static String generateSignature(Map<String, Object> map, String timestamp, String secretKey, String requestId, String token) { try { // 对参数进行排序 将params转成字符串再转成map是为了将其中的对象和集合转成jsonObject和jsonArray 方便排序处理 // Map<String, Object> map = JSONObject.parseObject(JSONObject.toJSONString(map), Map.class); Map<String, Object> sortedParams = new TreeMap<>(map); sortParams(sortedParams); // 将参数转换为 JSON 字符串 String jsonString = JSONObject.toJSONString(sortedParams); // 计算参数的 MD5 String paramsMD5 = Md5Util.getMD5ByCharset(jsonString, StandardCharsets.UTF_8); log.info("generateSignature requestId:{} jsonString:{} paramsMD5:{}", requestId, jsonString, paramsMD5); // 生成待签名的数据 String dataToSign = PARAMS + "=" + paramsMD5 + "&" + TIMESTAMP_HEADER + "=" + timestamp + "&" + REQUEST_ID + "=" + requestId + "&" + TOKEN + "=" + token; // 使用 HMAC-SHA256 生成签名 return Md5Util.hmacSha256(dataToSign, secretKey); } catch (Exception e) { throw new BadRequestException("短信生成签名:" + e.getMessage()); } } /** * 解析 XML 响应并提取 <string> 标签中的值 * * @param xmlResponse XML 响应字符串 * @return <string> 标签中的值 */ public static String parseXmlResponse(String xmlResponse) { try { // 创建 DocumentBuilder DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); // 将 XML 字符串转换为 Document 对象 Document document = builder.parse(new ByteArrayInputStream(xmlResponse.getBytes(StandardCharsets.UTF_8))); // 获取 <string> 标签 NodeList nodeList = document.getElementsByTagName("string"); Element stringElement = (Element) nodeList.item(0); // 提取标签中的文本内容 return stringElement.getTextContent(); } catch (Exception e) { throw new BadRequestException("短信解析XML:" + e.getMessage()); } } } oying-system/src/main/java/com/oying/modules/winnerlook/util/Md5Util.java
New file @@ -0,0 +1,78 @@ package com.oying.modules.winnerlook.util; import com.oying.exception.BadRequestException; import org.apache.commons.codec.binary.Base64; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** * @author MinaWu * @description 加密 HMAC_SHA256、MD5 * @date 2025/3/31 14:41 */ public class Md5Util { // 用来将字节转换成 16 进制表示的字符 static char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; private static final String HMAC_SHA256 = "HmacSHA256"; /** * HMAC_SHA256加密 * @param data 待加密数据 * @param key 密钥 * @return String */ public static String hmacSha256(String data, String key) throws Exception { Mac mac = Mac.getInstance(HMAC_SHA256); SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), HMAC_SHA256); mac.init(secretKey); byte[] hmacBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); return Base64.encodeBase64String(hmacBytes); } /** * 对一段String生成MD5加密信息 * * @param message 要加密的String * @return 生成的MD5信息 */ public static String getMD5ByCharset(String message, Charset charset) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] b = md.digest(message.getBytes(charset)); return byteToHexString(b); } catch (NoSuchAlgorithmException e) { throw new BadRequestException("短信MD5加密:" + e.getMessage()); } } /** * 把byte[]数组转换成十六进制字符串表示形式 * * @param tmp 要转换的byte[] * @return 十六进制字符串表示形式 */ private static String byteToHexString(byte[] tmp) { String s; // 用字节表示就是 16 个字节 char[] str = new char[16 * 2]; // 每个字节用 16 进制表示的话,使用两个字符, // 所以表示成 16 进制需要 32 个字符 int k = 0; // 表示转换结果中对应的字符位置 for (int i = 0; i < 16; i++) { // 从第一个字节开始,对 MD5 的每一个字节 // 转换成 16 进制字符的转换 byte byte0 = tmp[i]; // 取第 i 个字节 str[k++] = hexDigits[byte0 >>> 4 & 0xf]; // 取字节中高 4 位的数字转换, // >>> 为逻辑右移,将符号位一起右移 str[k++] = hexDigits[byte0 & 0xf]; // 取字节中低 4 位的数字转换 } s = new String(str); // 换后的结果转换为字符串 return s; } } oying-system/src/main/resources/config/application-dev.yml
@@ -178,10 +178,10 @@ endpoint: https://obs.cn-southwest-2.myhuaweicloud.com winner-look: url-send-msg: https://118.178.116.15:8443/winnerrxd/api/trigger/SendMsg url-send-msg-batch: https://118.178.116.15:8443/winnerrxd/api/trigger/SendBatchMsg user-code: CQLYSXYJ user-pass: lych1205! url-send-msg: https://118.178.116.15/winnerrxd token: e2318b05713e473890f79fe83d18e235 user-code: SHOYYJ user-pass: Oy250928 # 支付类型 pay: oying-system/src/main/resources/config/application-prod.yml
@@ -193,10 +193,10 @@ endpoint: https://obs.cn-southwest-2.myhuaweicloud.com winner-look: url-send-msg: https://118.178.116.15:8443/winnerrxd/api/trigger/SendMsg url-send-msg-batch: https://118.178.116.15:8443/winnerrxd/api/trigger/SendBatchMsg user-code: CQLYSXYJ user-pass: lych1205! url-send-msg: https://118.178.116.15/winnerrxd token: e2318b05713e473890f79fe83d18e235 user-code: SHOYYJ user-pass: Oy250928 # 支付类型 pay: oying-system/src/main/resources/config/application.yml
@@ -23,7 +23,7 @@ check-template-location: false profiles: # 激活的环境,如果需要 quartz 分布式支持,需要修改 active: dev,quartz active: dev active: prod data: redis: repositories: