微信支付APIV3统一回调接口封装(H5、JSAPI、App、小程序)

这篇具有很好参考价值的文章主要介绍了微信支付APIV3统一回调接口封装(H5、JSAPI、App、小程序)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

😊 @ 作者: 一恍过去
💖 @ 主页: https://blog.csdn.net/zhuocailing3390
🎊 @ 社区: Java技术栈交流
🎉 @ 主题: 微信支付统一回调接口封装(H5、JSAPI、App、小程序)
⏱️ @ 创作时间: 2022年07月12日

微信支付回调接口,支付,微信,小程序

前言

对微信支付的H5、JSAPI、H5、App、小程序支付方式进行统一,此封装接口适用于普通商户模式支付,如果要进行服务商模式支付可以结合服务商官方API进行参数修改(未验证可行性)。

1、引入POM

        <!--微信支付SDK-->
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-apache-httpclient</artifactId>
            <version>0.4.7</version>
        </dependency>

2、配置Yaml

wxpay:
  #应用编号
  appId: xxxx
  #商户号
  mchId: xxx
  # APIv2密钥
  apiKey: xxxx
  # APIv3密钥
  apiV3Key: xxx
  # 微信支付V3-url前缀
  baseUrl: https://api.mch.weixin.qq.com/v3
  # 支付通知回调, pjm6m9.natappfree.cc 为内网穿透地址
  notifyUrl: http://pjm6m9.natappfree.cc/pay/payNotify
  # 退款通知回调, pjm6m9.natappfree.cc 为内网穿透地址
  refundNotifyUrl: http://pjm6m9.natappfree.cc/pay/refundNotify
  # 密钥路径,resources根目录下
  keyPemPath: apiclient_key.pem
  #商户证书序列号
  serialNo: xxxxx

3、配置密钥文件

在商户/服务商平台的”账户中心" => “API安全” 进行API证书、密钥的设置,API证书主要用于获取“商户证书序列号”以及“p12”、“key.pem”、”cert.pem“证书文件,j将获取的apiclient_key.pem文件放在项目的resources目录下。
微信支付回调接口,支付,微信,小程序

4、配置PayConfig

WechatPayConfig:


import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.exception.HttpCodeException;
import com.wechat.pay.contrib.apache.httpclient.exception.NotFoundException;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;

/**
 * @Author:
 * @Description:
 **/
@Component
@Data
@Slf4j
@ConfigurationProperties(prefix = "wxpay")
public class WechatPayConfig {
    /**
     * 应用编号
     */
    private String appId;
    /**
     * 商户号
     */
    private String mchId;
    /**
     * 服务商商户号
     */
    private String slMchId;
    /**
     * APIv2密钥
     */
    private String apiKey;
    /**
     * APIv3密钥
     */
    private String apiV3Key;
    /**
     * 支付通知回调地址
     */
    private String notifyUrl;
    /**
     * 退款回调地址
     */
    private String refundNotifyUrl;

    /**
     * API 证书中的 key.pem
     */
    private String keyPemPath;

    /**
     * 商户序列号
     */
    private String serialNo;

    /**
     * 微信支付V3-url前缀
     */
    private String baseUrl;

    /**
     * 获取商户的私钥文件
     * @param keyPemPath
     * @return
     */
    public PrivateKey getPrivateKey(String keyPemPath){

            InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(keyPemPath);
            if(inputStream==null){
                throw new RuntimeException("私钥文件不存在");
            }
            return PemUtil.loadPrivateKey(inputStream);
    }

    /**
     * 获取证书管理器实例
     * @return
     */
    @Bean
    public  Verifier getVerifier() throws GeneralSecurityException, IOException, HttpCodeException, NotFoundException {

        log.info("获取证书管理器实例");

        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(keyPemPath);

        //私钥签名对象
        PrivateKeySigner privateKeySigner = new PrivateKeySigner(serialNo, privateKey);

        //身份认证对象
        WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);

        // 使用定时更新的签名验证器,不需要传入证书
        CertificatesManager certificatesManager = CertificatesManager.getInstance();
        certificatesManager.putMerchant(mchId,wechatPay2Credentials,apiV3Key.getBytes(StandardCharsets.UTF_8));

        return certificatesManager.getVerifier(mchId);
    }


    /**
     * 获取支付http请求对象
     * @param verifier
     * @return
     */
    @Bean(name = "wxPayClient")
    public CloseableHttpClient getWxPayClient(Verifier verifier)  {

        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(keyPemPath);

        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(mchId, serialNo, privateKey)
                .withValidator(new WechatPay2Validator(verifier));

        // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
        return builder.build();
    }

    /**
     * 获取HttpClient,无需进行应答签名验证,跳过验签的流程
     */
    @Bean(name = "wxPayNoSignClient")
    public CloseableHttpClient getWxPayNoSignClient(){

        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(keyPemPath);

        //用于构造HttpClient
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                //设置商户信息
                .withMerchant(mchId, serialNo, privateKey)
                //无需进行签名验证、通过withValidator((response) -> true)实现
                .withValidator((response) -> true);

        // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
        return builder.build();
    }
}

微信支付回调接口,支付,微信,小程序

6、回调校验器

用于对微信支付成功后的回调数据进行签名验证,保证数据的安全性与真实性。

WechatPayValidator:


import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;

import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;

/**
 * @Author: 
 * @Description:
 **/
@Slf4j
public class WechatPayValidator {
    /**
     * 应答超时时间,单位为分钟
     */
    private static final long RESPONSE_EXPIRED_MINUTES = 5;
    private final Verifier verifier;
    private final String requestId;
    private final String body;


    public WechatPayValidator(Verifier verifier, String requestId, String body) {
        this.verifier = verifier;
        this.requestId = requestId;
        this.body = body;
    }

    protected static IllegalArgumentException parameterError(String message, Object... args) {
        message = String.format(message, args);
        return new IllegalArgumentException("parameter error: " + message);
    }

    protected static IllegalArgumentException verifyFail(String message, Object... args) {
        message = String.format(message, args);
        return new IllegalArgumentException("signature verify fail: " + message);
    }

    public final boolean validate(HttpServletRequest request)  {
        try {
            //处理请求参数
            validateParameters(request);

            //构造验签名串
            String message = buildMessage(request);

            String serial = request.getHeader(WECHAT_PAY_SERIAL);
            String signature = request.getHeader(WECHAT_PAY_SIGNATURE);

            //验签
            if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
                throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
                        serial, message, signature, requestId);
            }
        } catch (IllegalArgumentException e) {
            log.warn(e.getMessage());
            return false;
        }

        return true;
    }

    private  void validateParameters(HttpServletRequest request) {

        // NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
        String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};

        String header = null;
        for (String headerName : headers) {
            header = request.getHeader(headerName);
            if (header == null) {
                throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
            }
        }

        //判断请求是否过期
        String timestampStr = header;
        try {
            Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
            // 拒绝过期请求
            if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
                throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
            }
        } catch (DateTimeException | NumberFormatException e) {
            throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
        }
    }

    private  String buildMessage(HttpServletRequest request)  {
        String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
        String nonce = request.getHeader(WECHAT_PAY_NONCE);
        return timestamp + "\n"
                + nonce + "\n"
                + body + "\n";
    }

    private  String getResponseBody(CloseableHttpResponse response) throws IOException {
        HttpEntity entity = response.getEntity();
        return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
    }

    /**
     * 对称解密,异步通知的加密数据
     * @param resource 加密数据
     * @param apiV3Key apiV3密钥
     * @param type 1-支付,2-退款
     * @return
     */
    public static  Map<String, Object> decryptFromResource(String resource,String apiV3Key,Integer type) {

        String msg = type==1?"支付成功":"退款成功";
        log.info(msg+",回调通知,密文解密");
        try {
        //通知数据
        Map<String, String> resourceMap = JSONObject.parseObject(resource, new TypeReference<Map<String, Object>>() {
        });
        //数据密文
        String ciphertext = resourceMap.get("ciphertext");
        //随机串
        String nonce = resourceMap.get("nonce");
        //附加数据
        String associatedData = resourceMap.get("associated_data");

        log.info("密文: {}", ciphertext);
        AesUtil aesUtil = new AesUtil(apiV3Key.getBytes(StandardCharsets.UTF_8));
        String resourceStr = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
                nonce.getBytes(StandardCharsets.UTF_8),
                ciphertext);

        log.info(msg+",回调通知,解密结果 : {}", resourceStr);
        return JSONObject.parseObject(resourceStr, new TypeReference<Map<String, Object>>(){});
    }catch (Exception e){
        throw new RuntimeException("回调参数,解密失败!");
        }
    }
}

7、回调Body内容处理

HttpUtils:

public class HttpUtils {

    /**
     * 将通知参数转化为字符串
     * @param request
     * @return
     */
    public static String readData(HttpServletRequest request) {
        BufferedReader br = null;
        try {
            StringBuilder result = new StringBuilder();
            br = request.getReader();
            for (String line; (line = br.readLine()) != null; ) {
                if (result.length() > 0) {
                    result.append("\n");
                }
                result.append(line);
            }
            return result.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

6、支付/退款回调通知

支付/退款回调通知:在进行支付或退款操作后,支付平台向商户发送的异步通知,用于告知支付或退款的结果和相关信息。这种回调通知是实现支付系统与商户系统之间数据同步和交互的重要方式。

支付/退款回调通知包含一些关键信息,如订单号、交易金额、支付/退款状态、支付平台的交易流水号等。

在回调后,通过解析回调数据,一定需要验证签名的合法性。

NotifyController:


import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.lhz.demo.pay.WechatPayConfig;
import com.lhz.demo.pay.WechatPayValidator;
import com.lhz.demo.utils.HttpUtils;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;


/**
 * @Author: 
 * @Description:
 **/
@Api(tags = "回调接口(API3)")
@RestController
@Slf4j
public class NotifyController {

    @Resource
    private WechatPayConfig wechatPayConfig;

    @Resource
    private Verifier verifier;

    private final ReentrantLock lock = new ReentrantLock();

    @ApiOperation(value = "支付回调", notes = "支付回调")
    @ApiOperationSupport(order = 5)
    @PostMapping("/payNotify")
    public Map<String, String> payNotify(HttpServletRequest request, HttpServletResponse response) {
        log.info("支付回调");

        // 处理通知参数
        Map<String,Object> bodyMap = getNotifyBody(request);
        if(bodyMap==null){
            return falseMsg(response);
        }

        log.warn("=========== 在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱 ===========");
        if(lock.tryLock()) {
            try {
                // 解密resource中的通知数据
                String resource = bodyMap.get("resource").toString();
                Map<String, Object> resourceMap = WechatPayValidator.decryptFromResource(resource, wechatPayConfig.getApiV3Key(),1);
                String orderNo = resourceMap.get("out_trade_no").toString();
                String transactionId = resourceMap.get("transaction_id").toString();

                // TODO 根据订单号,做幂等处理,并且在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱
                log.warn("=========== 根据订单号,做幂等处理 ===========");
            } finally {
                //要主动释放锁
                lock.unlock();
            }
        }

        //成功应答
        return trueMsg(response);
    }


    @ApiOperation(value = "退款回调", notes = "退款回调")
    @ApiOperationSupport(order = 5)
    @PostMapping("/refundNotify")
    public Map<String, String> refundNotify(HttpServletRequest request, HttpServletResponse response) {
        log.info("退款回调");

        // 处理通知参数
        Map<String,Object> bodyMap = getNotifyBody(request);
        if(bodyMap==null){
            return falseMsg(response);
        }

        log.warn("=========== 在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱 ===========");
        if(lock.tryLock()) {
            try {
                // 解密resource中的通知数据
                String resource = bodyMap.get("resource").toString();
                Map<String, Object> resourceMap = WechatPayValidator.decryptFromResource(resource, wechatPayConfig.getApiV3Key(),2);
                String orderNo = resourceMap.get("out_trade_no").toString();
                String transactionId = resourceMap.get("transaction_id").toString();

                // TODO 根据订单号,做幂等处理,并且在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱

                log.warn("=========== 根据订单号,做幂等处理 ===========");
            } finally {
                //要主动释放锁
                lock.unlock();
            }
        }

        //成功应答
        return trueMsg(response);
    }

    private Map<String,Object> getNotifyBody(HttpServletRequest request){
        //处理通知参数
        String body = HttpUtils.readData(request);
        log.info("退款回调参数:{}",body);

        // 转换为Map
        Map<String, Object> bodyMap = JSONObject.parseObject(body, new TypeReference<Map<String, Object>>(){});
        // 微信的通知ID(通知的唯一ID)
        String notifyId = bodyMap.get("id").toString();

        // 验证签名信息
        WechatPayValidator wechatPayValidator
                = new WechatPayValidator(verifier, notifyId, body);
        if(!wechatPayValidator.validate(request)){

            log.error("通知验签失败");
            return null;
        }
        log.info("通知验签成功");
        return bodyMap;
    }

    private Map<String, String> falseMsg(HttpServletResponse response){
        Map<String, String> resMap = new HashMap<>(8);
        //失败应答
        response.setStatus(500);
        resMap.put("code", "ERROR");
        resMap.put("message", "通知验签失败");
        return resMap;
    }

    private Map<String, String> trueMsg(HttpServletResponse response){
        Map<String, String> resMap = new HashMap<>(8);
        //成功应答
        response.setStatus(200);
        resMap.put("code", "SUCCESS");
        resMap.put("message", "成功");
        return resMap;
    }
}

微信支付回调接口,支付,微信,小程序文章来源地址https://www.toymoban.com/news/detail-717500.html

到了这里,关于微信支付APIV3统一回调接口封装(H5、JSAPI、App、小程序)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 微信支付,JSAPI支付,APP支付,H5支付,Native支付,小程序支付功能详情以及回调处理

    支付wiki: https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml 支付api: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/index.shtml 开发工具包(SDK)下载: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay6_0.shtml#part-1 1.1简介 JSAPI支付是指商户通过调用微信支付提供的JSAPI接口,在支付场景中调起微信支付模

    2023年04月18日
    浏览(49)
  • uniapp+java/springboot实现微信小程序APIV3支付功能

    微信小程序的支付跟H5的支付和APP支付流程不一样,本次只描述下小程序支付流程。 1.微信小程序账号 文档:小程序申请 小程序支付需要先认证,如果你有已认证的公众号,也可以通过公众号免费注册认证小程序。 一般300元,我是认证的政府的免费。 然后登录小程序,设置

    2023年04月19日
    浏览(47)
  • 微信支付apiV3异常:The corresponding provider for the merchant already exists

    异常信息 原因 这个错误是微信SDK抛出的,这是因为微信支付apiV3的RSAConfig重复build导致,即RSAConfig要保证是 单例 才不会导致报错。 参数说明 mchId:商户号 privateKey:商户号密钥 mchSerialNo:商户证书号 apiV3Key:apiV3密钥 建议 可以把商户配置参数使用数据库保存,服务启动的时

    2024年02月11日
    浏览(60)
  • 【微信小程序】Java实现微信支付(小程序支付JSAPI-V3)java-sdk工具包(包含支付出现的多次回调的问题解析,接口幂等性)

          对于一个没有写过支付的小白,打开微信支付官方文档时彻底懵逼 ,因为 微信支付文档太过详细, 导致我无从下手,所以写此文章,帮助第一次写支付的小伙伴梳理一下。 一、流程分为三个接口:(这是前言,先看一遍,保持印象,方便理解代码) 1、第一个接口:

    2024年01月16日
    浏览(77)
  • java对接微信支付:JSAPI支付成功之“微信回调”

    承接上一篇微信支付,现在简单说一下 微信支付回调 目录 一、支付回调 二、微信回调地址问题 1.本地/上线测试 2.控制器调用接口(代码) 总结 当用户支付成功之后,支付平台会向我们指定的服务器接口发送请求传递订单支付状态数据 如果你是再本地进行测试,那就需要使用

    2024年02月12日
    浏览(41)
  • 微信的 h5 支付和 jsapi 支付

    申请地址: https://pay.weixin.qq.com/ 如果你还没有微信商户号,请点击上面的链接进行申请,如果已经有了,可以跳过这一步 首先点击 账户中心 ▶ API安全 ▶ 申请API证书 申请详细步骤: https://kf.qq.com/faq/161222NneAJf161222U7fARv.html 首先点击 账户中心 ▶ API安全 ▶ 设置APIv3密钥 ▶

    2024年02月13日
    浏览(46)
  • 〔支付接入〕微信的 h5 支付和 jsapi 支付

    申请地址: https://pay.weixin.qq.com/ 如果你还没有微信商户号,请点击上面的链接进行申请,如果已经有了,可以跳过这一步 首先点击 账户中心 ▶ API安全 ▶ 申请API证书 申请详细步骤: https://kf.qq.com/faq/161222NneAJf161222U7fARv.html 首先点击 账户中心 ▶ API安全 ▶ 设置APIv3密钥 ▶

    2024年02月13日
    浏览(52)
  • 微信支付 H5端 和小程序端 统一下单接口 4个JAVA源码文件代码

    首先来看看官方支付文档的一些相关信息  1、用户在商户侧完成下单,使用微信支付进行支付 2、由商户后台向微信支付发起下单请求(调用统一下单接口)注:交易类型trade_type=MWEB 3、统一下单接口返回支付相关参数给商户后台,如支付跳转url(参数名“mweb_url”),商户通

    2024年02月08日
    浏览(43)
  • 微信H5支付及通知回调

    1.在微信商户平台中进行登录并申请相关功能和配置 1.1微信商户平台https://pay.weixin.qq.com/index.php/core/home/loginreturn_url=%2F 登录并配置,在商户平台上 - 产品中心 - 开通相关的产品,比如我这里使用的是 H5支付 1.2 然后配置相关的参数 2.1导入相关依赖 2.2 代码编写 2.3 编写H5支付回调

    2024年02月12日
    浏览(44)
  • java对接微信支付:JSAPI支付(微信公众号支付)

    本文是【微信JSAPI支付】文章,主要讲解商户对接微信支付,简洁版测试 文章目录 一、JSAPI支付接入前准备 二、代码片段 1.引入Maven依赖 2.后端业务请求接口 3.前端调起支付请求方法 总结 1、JSAPI支付首先需要注册、认证一个公众号(大概300块一年) 微信公众号注册 2、申请成为

    2024年02月08日
    浏览(62)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包