微信支付-通知验签始终不通过的问题

记一次微信支付验签始终异常问题,折腾了两宿,最后是 body 有问题 o(╥﹏╥)o

这里使用的是

 <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-java</artifactId>
            <version>0.2.1</version>
</dependency>

噩梦的开始与问题的发现

我最开始的写法:

public Map<String, String> nation(HttpServletRequest request, 
							      @RequestBody PayResult payResult) {
 RequestParam requestParam = new RequestParam.Builder()
                .serialNumber(payConfig.getTenantAccount().getMerchantSerialNumber())
                .nonce(payResult.getResource().getNonce())
                .signature(signature)
                .timestamp(timestamp)
                // 若未设置signType,默认值为 WECHATPAY2-SHA256-RSA2048
                .signType(signType)
                .body(JSON.toJSONString(payResult))
                .build();
}

我这里是使用 RequestBody 接收的参数,因为官方文档说明通知参数类型为 application/json,如此解析并没有问题,也可以获取到微信发过来的数据
但是问题也是出在这里,o(╥﹏╥)o,无意间搜到了这篇文章https://developers.weixin.qq.com/community/pay/doc/00020ad2448618e57a1b5faf250800
里面提到,微信验签时 请求体 的字符串顺序不能发生任何改变,以退款通知为例,验签时传入的对象键值对顺序必须与为文档保持一致:

{
    "id":"EV-2018022511223320873",
    "create_time":"2018-06-08T10:34:56+08:00",
    "resource_type":"encrypt-resource",
    "event_type":"REFUND.SUCCESS",
    "summary":"退款成功",
    "resource" : {
        "original_type": "refund",
        "algorithm":"AEAD_AES_256_GCM",
        "ciphertext": "...",
        "associated_data": "",
        "nonce": "..."
    }
}

@RequestBody 在解析参数时会改变参数顺序,我用 .body(JSON.toJSONString(payResult)) 传进去就出了问题

问题的解决

所以最后只能在HTTP的流中获取请求体

	@PostMapping("nation")
    @NoAuth
    public Map<String, String> nation(HttpServletRequest request) throws IOException {
        VerdureRequestWrapper requestWrapper = new VerdureRequestWrapper(request);
        logger.info("接收微信通知:");
        logger.info(requestWrapper.getRequestParams());
        String signature = RequestUtils.getHeader(request, "Wechatpay-Signature");
        String timestamp = RequestUtils.getHeader(request, "Wechatpay-Timestamp");
        String signType = RequestUtils.getHeader(request, "Wechatpay-Signature-Type");
        String serial = RequestUtils.getHeader(request, "Wechatpay-Serial");
        String nonce = RequestUtils.getHeader(request, "Wechatpay-Nonce");
        logger.info("===================================================:");
        Enumeration<String> headerNames = request.getHeaderNames();
        while(headerNames.hasMoreElements()){
            String element = headerNames.nextElement();
            System.out.println(element+":"+request.getHeader(element));
        }
        PayResult payResult = JSON.parseObject(requestWrapper.getRequestParams(), PayResult.class);
        // 构造 RequestParam
        // 获取HTTP请求头中的 Wechatpay-Signature 、 Wechatpay-Nonce 、 Wechatpay-Timestamp 、 Wechatpay-Serial 、 Request-ID 、Wechatpay-Signature-Type 对应的值,构建 RequestParam 。
        RequestParam requestParam = new RequestParam.Builder()
                .serialNumber(serial)
                .nonce(nonce)
                .signature(signature)
                .timestamp(timestamp)
                // 若未设置signType,默认值为 WECHATPAY2-SHA256-RSA2048
                .signType(signType)
                .body(requestWrapper.getRequestParams())
                .build();

        // 初始化 NotificationConfig
        NotificationConfig rsaNotificationConfig = new RSANotificationConfig.Builder()
                .apiV3Key(this.payConfig.getTenantAccount().getApiV3Key())
                .certificates(this.payConfig.getWechatCertificate())
                .build();
        // 初始化 NotificationParser
        NotificationParser parser=new NotificationParser(rsaNotificationConfig);
        WePayNation nation = new WePayNation();
        nation.setSummary(payResult.getSummary());
        nation.setResourceType(payResult.getResource_type());
        nation.setRequestData(JSON.toJSONString(payResult.getResource()));
        nation.setOriginalType(payResult.getResource().getOriginal_type());
        nation.setNationTime(payResult.getCreate_time());
        nation.setNationId(payResult.getId());
        nation.setEventType(payResult.getEvent_type());
        try {
            switch (payResult.getEvent_type()) {
                case "TRANSACTION.SUCCESS": // 支付通知
                    // 验签并解密报文
                    Transaction decryptObject = parser.parse(requestParam, Transaction.class);
                    nation.setDecodeData(JSON.toJSONString(decryptObject));
                    // 调用接口处理支付结果
                    payService.paySuccess(decryptObject);
                    // 通知业务服务
                    MinAppHolderReader.app().paySuccess(decryptObject);
                    break;
                case "REFUND.SUCCESS": // 退款成功通知
                case "REFUND.ABNORMAL": // 退款异常通知
                case "REFUND.CLOSED": // 退款关闭通知
                    RefundNotification refundNotification = parser.parse(requestParam, RefundNotification.class);
                    nation.setDecodeData(JSON.toJSONString(refundNotification));
                    switch (payResult.getEvent_type()) {
                        case "REFUND.SUCCESS": // 退款成功通知
                            MinAppHolderReader.app().refundSuccess(refundNotification);
                            payService.refundSuccess(refundNotification, 8, "退款成功");
                            break;
                        case "REFUND.ABNORMAL": // 退款异常通知
                            MinAppHolderReader.app().refundError(refundNotification);
                            payService.refundSuccess(refundNotification, 9, "退款异常");
                            break;
                        case "REFUND.CLOSED": // 退款关闭通知
                            MinAppHolderReader.app().refundClose(refundNotification);
                            payService.refundSuccess(refundNotification, 10, "退款关闭");
                            break;
                    }
                    break;
            }
        } catch (Exception e) {
            e.printStackTrace();
            loggerApi.error("微信通知接收解密处理时发生异常", e);
            Map<String, String> res = new HashMap<>();
            res.put("code", "FAIL");
            res.put("message", e.getMessage());
            return res;
        } finally {
            nation.setSystemCreateUser("WX");
            payNationMapper.insert(nation);
        }
        Map<String, String> res = new HashMap<>();
        res.put("code", "SUCCESS");
        res.put("message", "成功");
        return res;
    }

我我系统内部因为有重构 httpServletRequest 的方法直接复用了,也可以使用其他方式获取 POST 请求体

若使用自定义方法读取 post 请求体,就不能再使用 @RquestBody 解析参数了,因为 HttpServletRequest 请求体只允许被读取一次!!


public class VerdureRequestWrapper extends HttpServletRequestWrapper {
        HttpServletRequest orgRequest = null;
        private byte[] bytes;
        private WrappedServletInputStream  wrappedServletInputStream;
        public VerdureRequestWrapper(HttpServletRequest request) throws IOException {
            super(request);
            this.orgRequest = request;
            //读取输入流的请求参数,保存到bytes中
            bytes = IOUtils.toByteArray(request.getInputStream());
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
            this.wrappedServletInputStream = new WrappedServletInputStream(byteArrayInputStream);

            //把post参数重新写入请求流
            reWriteInputStream();
        }
        public void setRequestParams(String json) {
            wrappedServletInputStream.setStream(new ByteArrayInputStream(json.getBytes()));
        }
        /**
         * 把参数重新写进请求里
         */
        public void reWriteInputStream() {
            wrappedServletInputStream.setStream(new ByteArrayInputStream(bytes != null ? bytes : new byte[0]));
        }
        @Override
        public ServletInputStream getInputStream() throws IOException {
            return wrappedServletInputStream;
        }
        @Override
        public BufferedReader getReader() throws IOException {
            return new BufferedReader(new InputStreamReader(wrappedServletInputStream));
        }
        /**
         * 获取post参数
         */
        public String getRequestParams() throws IOException {
            return new String(bytes, this.getCharacterEncoding());
        }
        //清洗参数,防止xss注入
        public String[] getParameterValues(String parameter) {
            String[] values = super.getParameterValues(parameter);
            if (values == null) {
                return null;
            }
            int count = values.length;
            String[] encodedValues = new String[count];
            for (int i = 0; i < count; i++) {
                encodedValues[i] = xssEncode(values[i]);
            }
            return encodedValues;
        }
        public String getParameter(String name){
            String value = super.getParameter(xssEncode(name));
            if (value != null) {
                value = xssEncode(value);
            }
            return value;
        }
        public String getHeader(String name) {
            String value = super.getHeader(xssEncode(name));
            if (value != null) {
                value = xssEncode(value);
            }
            return value;
        }
        private static String xssEncode(String s){
            return s;
        }
        public HttpServletRequest getOrgRequest(){
            return this.orgRequest;
        }
        private class WrappedServletInputStream extends ServletInputStream {
            public void setStream(InputStream stream) {
                this.stream = stream;
            }
            private InputStream stream;
            public WrappedServletInputStream(InputStream stream) {
                this.stream = stream;
            }
            public int read() throws IOException {
                return stream.read();
            }
            public boolean isFinished() {
                return true;
            }
            public boolean isReady() {
                return true;
            }
            public void setReadListener(ReadListener readListener) {
            }
        }
    }
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值