|
|
@@ -4,13 +4,18 @@ import com.alibaba.fastjson.JSON;
|
|
|
import com.alibaba.fastjson.JSONObject;
|
|
|
import com.etotem.cfc.service.VirtualPayService;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
import org.springframework.web.bind.annotation.PostMapping;
|
|
|
import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
+import org.springframework.web.bind.annotation.RequestMethod;
|
|
|
+import org.springframework.web.bind.annotation.RequestParam;
|
|
|
import org.springframework.web.bind.annotation.RestController;
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
|
import javax.servlet.http.HttpServletRequest;
|
|
|
import java.io.BufferedReader;
|
|
|
+import java.security.MessageDigest;
|
|
|
+import java.util.Arrays;
|
|
|
import java.util.HashMap;
|
|
|
import java.util.Map;
|
|
|
|
|
|
@@ -26,10 +31,39 @@ public class VirtualPayController {
|
|
|
@Resource
|
|
|
private VirtualPayService virtualPayService;
|
|
|
|
|
|
+ /** 微信消息推送 Token(后台【开发管理-开发设置-消息推送】配置,URL 验证与推送签名校验用) */
|
|
|
+ @Value("${wechat.virtual-pay-token:}")
|
|
|
+ private String pushToken;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 微信服务器 URL 验证(配置消息推送 URL 时微信发 GET 请求校验,原样返回 echostr 即验证通过)。
|
|
|
+ * 微信平台强制要求 GET 校验,属"统一 POST"规范唯一例外。
|
|
|
+ */
|
|
|
+ @RequestMapping(value = "/notify", method = RequestMethod.GET)
|
|
|
+ public String verifyUrl(@RequestParam("signature") String signature,
|
|
|
+ @RequestParam("timestamp") String timestamp,
|
|
|
+ @RequestParam("nonce") String nonce,
|
|
|
+ @RequestParam("echostr") String echostr) {
|
|
|
+ if (checkSignature(signature, timestamp, nonce)) {
|
|
|
+ return echostr;
|
|
|
+ }
|
|
|
+ log.warn("虚拟支付消息推送URL验证签名不匹配: signature={}", signature);
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
/** 微信虚拟支付发货/退款等消息推送(xpay_goods_deliver_notify / xpay_refund_notify / 其他) */
|
|
|
@PostMapping("/notify")
|
|
|
public Map<String, Object> notify(HttpServletRequest request) {
|
|
|
try {
|
|
|
+ // 0. 微信消息签名校验(防伪造推送):signature = sha1(sort(token, timestamp, nonce))
|
|
|
+ String signature = request.getParameter("signature");
|
|
|
+ String timestamp = request.getParameter("timestamp");
|
|
|
+ String nonce = request.getParameter("nonce");
|
|
|
+ if (!checkSignature(signature, timestamp, nonce)) {
|
|
|
+ log.error("虚拟支付消息推送签名校验失败,拒绝处理: signature={}, timestamp={}, nonce={}", signature, timestamp, nonce);
|
|
|
+ return failResult("signature check failed");
|
|
|
+ }
|
|
|
+
|
|
|
// 读原始 body(getReader(),参照 ProductOrderController.notify() L120-155 范式)
|
|
|
BufferedReader reader = request.getReader();
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
@@ -108,6 +142,49 @@ public class VirtualPayController {
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
+ /** 微信协议失败应答(非0触发微信重试) */
|
|
|
+ private Map<String, Object> failResult(String errMsg) {
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
+ result.put("ErrCode", -1);
|
|
|
+ result.put("ErrMsg", errMsg);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 微信消息签名校验:signature = sha1(字典序排列 [token, timestamp, nonce] 后拼接的字符串)。
|
|
|
+ * token 未配置时校验失败(防止裸奔收推送)。
|
|
|
+ */
|
|
|
+ private boolean checkSignature(String signature, String timestamp, String nonce) {
|
|
|
+ if (pushToken == null || pushToken.isEmpty()) {
|
|
|
+ log.error("虚拟支付消息推送 Token 未配置(wechat.virtual-pay-token),拒绝处理");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (signature == null || timestamp == null || nonce == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String[] arr = {pushToken, timestamp, nonce};
|
|
|
+ Arrays.sort(arr);
|
|
|
+ String plain = arr[0] + arr[1] + arr[2];
|
|
|
+ String expect = sha1Hex(plain);
|
|
|
+ return expect.equalsIgnoreCase(signature);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** SHA-1 十六进制摘要(微信消息推送签名用) */
|
|
|
+ private String sha1Hex(String input) {
|
|
|
+ try {
|
|
|
+ MessageDigest md = MessageDigest.getInstance("SHA-1");
|
|
|
+ byte[] digest = md.digest(input.getBytes("UTF-8"));
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ for (byte b : digest) {
|
|
|
+ sb.append(String.format("%02x", b));
|
|
|
+ }
|
|
|
+ return sb.toString();
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("SHA-1 计算异常", e);
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/** iOS 退款问询最低响应(协议规定应答字段,未接入时默认不同意由 Apple 裁决) */
|
|
|
private Map<String, Object> iosRefundBaseResponse() {
|
|
|
Map<String, Object> result = new HashMap<>();
|