From 2ae7676ed7d2585a3e9637bcab27691273c993c9 Mon Sep 17 00:00:00 2001 From: quyixiao <2621048238@qq.com> Date: Sat, 24 Jan 2026 13:49:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/data/utils/WeChatCallbackUtil.java | 116 +++++++++++++++++ .../vv/AppWeiXinCustomerNotifyController.java | 121 +++++++++++++----- 2 files changed, 208 insertions(+), 29 deletions(-) create mode 100644 api-mapper/src/main/java/com/heyu/api/data/utils/WeChatCallbackUtil.java 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/controller/vv/AppWeiXinCustomerNotifyController.java b/api-web/api-interface/src/main/java/com/heyu/api/controller/vv/AppWeiXinCustomerNotifyController.java index 169ebad..2525b28 100644 --- 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 @@ -1,48 +1,111 @@ package com.heyu.api.controller.vv; -import com.heyu.api.common.annotation.Describe; -import com.heyu.api.data.dao.vv.VvTradeOrderDao; -import com.heyu.api.jsapi.JsapiPrepay; +import com.heyu.api.data.utils.WeChatCallbackUtil; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; @Slf4j @RestController -@RequestMapping("/app/weixin") +@RequestMapping("/app/weixin/customer") public class AppWeiXinCustomerNotifyController { - - // 微信支付APIv3密钥 - @Value("${eb.config.weixin.pay.apiv3key}") - private String apiv3key; - - @Autowired - private VvTradeOrderDao tradeOrderDao; - - @Value("${eb.config.weixin.pay.mchid}") - private String mchid; + public static String token = "9Iosbni8qAU8Kcmy8tgRLw"; - @Autowired - private JsapiPrepay jsapiPrepay; - - @Describe("微信支付回调") - @PostMapping("/customer/notify") - public String customerNotify(@RequestBody String requestBody) { + public static String encodingAESKey = "miBhTSRiwCgn9aYoa8d1TDu62GYFV2aE03vXl2Rrgv6"; - - return "sucess"; - + /** + * 验证回调 URL (GET 请求) + */ + @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 请求) + */ + @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(); + } +} + }