diff --git a/api-mapper/src/main/java/com/heyu/api/data/utils/WeChatCallbackUtil.java b/api-mapper/src/main/java/com/heyu/api/data/utils/WeChatCallbackUtil.java new file mode 100644 index 0000000..e3bc641 --- /dev/null +++ b/api-mapper/src/main/java/com/heyu/api/data/utils/WeChatCallbackUtil.java @@ -0,0 +1,116 @@ +package com.heyu.api.data.utils; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.Base64; + +/** + * 微信回调验证和加解密工具类 + */ +public class WeChatCallbackUtil { + + /** + * 验证回调 URL + * + * @param token Token + * @param timestamp 时间戳 + * @param nonce 随机字符串 + * @param msgSignature 消息签名 + * @param echostr 加密的随机字符串 + * @param encodingAESKey EncodingAESKey + * @return 解密后的 echostr + * @throws Exception 验证失败或解密失败 + */ + public static String verifyURL(String token, String timestamp, + String nonce, String msgSignature, + String echostr, String encodingAESKey) throws Exception { + // 1. 验证签名 + String signature = generateSignature(token, timestamp, nonce, echostr); + if (!signature.equals(msgSignature)) { + throw new Exception("签名验证失败"); + } + + // 2. 解密 echostr + String result = decrypt(echostr, encodingAESKey); + return result; + } + + /** + * 生成签名 + */ + public static String generateSignature(String token, String timestamp, + String nonce, String echostr) throws Exception { + // 将 token、timestamp、nonce、echostr 按字典序排序 + String[] arr = {token, timestamp, nonce, echostr}; + Arrays.sort(arr); + + // 拼接字符串 + StringBuilder sb = new StringBuilder(); + for (String s : arr) { + sb.append(s); + } + + // SHA1 加密 + MessageDigest md = MessageDigest.getInstance("SHA-1"); + byte[] digest = md.digest(sb.toString().getBytes(StandardCharsets.UTF_8)); + + // 转换为十六进制字符串 + StringBuilder hexString = new StringBuilder(); + for (byte b : digest) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + + return hexString.toString(); + } + + /** + * AES 解密 + */ + public static String decrypt(String encryptedData, String encodingAESKey) throws Exception { + // Base64 解码 + byte[] keyBytes = Base64.getDecoder().decode(encodingAESKey); + byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData); + + // 提取前 16 字节作为 IV + byte[] iv = new byte[16]; + System.arraycopy(encryptedBytes, 0, iv, 0, 16); + + // 提取加密内容 + byte[] ciphertext = new byte[encryptedBytes.length - 16]; + System.arraycopy(encryptedBytes, 16, ciphertext, 0, ciphertext.length); + + // AES-256-CBC 解密 + SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES"); + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec); + + byte[] decrypted = cipher.doFinal(ciphertext); + + // 去除填充并提取消息内容 + // 前 4 字节是消息长度,最后是随机字符串 + int contentLength = bytesToInt(decrypted, 0); + byte[] content = new byte[contentLength]; + System.arraycopy(decrypted, 4, content, 0, contentLength); + + return new String(content, StandardCharsets.UTF_8); + } + + /** + * 字节数组转整数(大端序) + */ + private static int bytesToInt(byte[] bytes, int offset) { + return ((bytes[offset] & 0xFF) << 24) | + ((bytes[offset + 1] & 0xFF) << 16) | + ((bytes[offset + 2] & 0xFF) << 8) | + (bytes[offset + 3] & 0xFF); + } +} \ No newline at end of file diff --git a/api-web/api-interface/src/main/java/com/heyu/api/aop/ZhenZhenLogAop.java b/api-web/api-interface/src/main/java/com/heyu/api/aop/ZhenZhenLogAop.java index d1cc20d..0ed1db9 100644 --- a/api-web/api-interface/src/main/java/com/heyu/api/aop/ZhenZhenLogAop.java +++ b/api-web/api-interface/src/main/java/com/heyu/api/aop/ZhenZhenLogAop.java @@ -61,7 +61,7 @@ public class ZhenZhenLogAop { public final static List not_login_urls = Arrays.asList(user_login_url, anonymous_login_url, - "/app/weixin/payNotify","/app/weixin/refundNotify"); + "/app/weixin/payNotify","/app/weixin/refundNotify","/app/weixin/customer/verify","/app/weixin/customer/receiveMessage"); public Map classHasAnnotation = new HashMap(); diff --git a/api-web/api-interface/src/main/java/com/heyu/api/controller/vv/AppWeiXinCustomerNotifyController.java b/api-web/api-interface/src/main/java/com/heyu/api/controller/vv/AppWeiXinCustomerNotifyController.java new file mode 100644 index 0000000..3d28d91 --- /dev/null +++ b/api-web/api-interface/src/main/java/com/heyu/api/controller/vv/AppWeiXinCustomerNotifyController.java @@ -0,0 +1,115 @@ +package com.heyu.api.controller.vv; + + +import com.heyu.api.data.utils.WeChatCallbackUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +@Slf4j +@RestController +@RequestMapping("/app/weixin/customer") +public class AppWeiXinCustomerNotifyController { + + + + public static String token = "9Iosbni8qAU8Kcmy8tgRLw"; + + + public static String encodingAESKey = "miBhTSRiwCgn9aYoa8d1TDu62GYFV2aE03vXl2Rrgv6"; + + + /** + * 验证回调 URL (GET 请求) + */ + // https://api.1024api.com/api-interface/app/weixin/customer/verify?msg_signature=5392430904602161909×tamp=1737681600&nonce=1234567890&echostr=1234567890 + + // https://api.1024api.com/api-interface/app/weixin/customer/verify + + @GetMapping("/verify") + public String verify(@RequestParam("msg_signature") String msgSignature, + @RequestParam("timestamp") String timestamp, + @RequestParam("nonce") String nonce, + @RequestParam("echostr") String echostr) { + try { + // 验证并解密 + String result = WeChatCallbackUtil.verifyURL( + token, timestamp, nonce, msgSignature, echostr, encodingAESKey + ); + return result; + } catch (Exception e) { + e.printStackTrace(); + return "验证失败"; + } + } + + + + + /** + * 接收回调消息 (POST 请求) + */ + // /app/weixin/customer/receiveMessage + @PostMapping("/receiveMessage") + public String receiveMessage(@RequestParam("msg_signature") String msgSignature, + @RequestParam("timestamp") String timestamp, + @RequestParam("nonce") String nonce, + HttpServletRequest request) { + try { + // 读取请求体 + String encryptedMessage = getRequestBody(request); + + // 验证签名 + if (!verifySignature(msgSignature, timestamp, nonce, encryptedMessage)) { + return "签名验证失败"; + } + + // 解密消息 + String decryptedMessage = WeChatCallbackUtil.decrypt( + encryptedMessage, encodingAESKey + ); + + // 解析 XML 消息 + // TODO: 解析消息内容并处理 + + // 返回成功响应 + return "success"; + + } catch (Exception e) { + e.printStackTrace(); + return "处理失败"; + } + } + + /** + * 验证签名 + */ + private boolean verifySignature(String msgSignature, String timestamp, + String nonce, String encryptedMessage) { + try { + String signature = WeChatCallbackUtil.generateSignature( + token, timestamp, nonce, encryptedMessage + ); + return signature.equals(msgSignature); + } catch (Exception e) { + return false; + } + } + + /** + * 获取请求体内容 + */ + private String getRequestBody(HttpServletRequest request) throws IOException { + StringBuilder sb = new StringBuilder(); + try (java.io.BufferedReader reader = request.getReader()) { + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + } + return sb.toString(); + } +} +