文章目录

  • 前言
  • 一、微信支付流程理解
  • 1、流程图
  • 二、调用接口准备
  • 1、初始化微信连接
  • 2、调用微信统一下单接口
  • 3、后端生成签名
  • 三、微信回调通知
  • 1.微信验签
  • 2、数据解密
  • 总结



前言

最近在开发一款小程序用到微信支付,中间还是遇到了一些小坑,在这里记录一下,希望可以给到第一次做支付的童鞋一些帮助,此教程以小程序支付为例,其他支付与此类似


一、微信支付流程理解

1、流程图

我们在做到微信支付时,首先需要理解微信支付的整个流程,官方文档这里已经介绍的很详细了,直接上图:

android 微信支付介入文档 微信支付api接口文档_微信


开发步骤逻辑说明 :(以下每个步骤会在后续中详细讲解)

1、用户在小程序端调用后台自己写的下单接口,在这个接口中生成商户自己的订单。

2、后端在自己写的下单接口中 调用微信的统一下单接口,微信会返回一个预支付订单id。

3、后端在收到微信返回的预支付订单id后,需要将数据进行加签处理,并将数据返回给前端小程序。

4、小程序调起微信支付接口,其中参数见微信官方文档(注意:小程序再在起微信支付接口所需要的参数是后端签名后返回的数据)。

5、用户授权且支付成功后,微信会根据后端在预下单时填写的支付回调地址,异步通知商户后端支付信息。

6、后端在收到微信支付通知时,需要进行验签处理。验签通过后,后端执行自己的后续支付成功逻辑。

  • 后端再收到微信通知后,将收到结果反馈,否则微信服务器会再次发送通知。后端也需要校验当前支付信息是否已经接收并处理。如果已处理则不需要再次处理

二、调用接口准备

1、初始化微信连接

微信官方提供了相关的SDK,我们用微信提供的SDK就可以了,不要在看其他的各种自己写的连接教程、加签验签、调用证书等等。直接使用官方提供的SDK省时省力还安全。/font>
官方地址:微信支付API v3的Apache HttpClient扩展,实现了请求签名的生成和应答签名的验证

1、初始化HttpClient,我们直接使用官方推荐的方法:自动更新证书功能(不用考虑证书的问题)
使用 AutoUpdateCertificatesVerifier 类替代默认的验签器。它会在构造时自动下载商户对应的微信支付平台证书,并每隔一段时间(默认为1个小时)更新证书。

代码如下(示例):
具体参数参考官方文档:微信支付统一下单

/**
     *  初始化微信支付链接
     *  privateKey  商户API私钥
     *  apiv3  API v3密钥
     *  mchid  商户id
     *  serialNumber 商户API证书的证书序列号
     * @return
     */
    public HttpClient initWChant(){
        PrivateKey merchantPrivateKey = PemUtil
                .loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes(StandardCharsets.UTF_8)));
        String apiv3 = WChantPay.getApiv3();
        String mchid = WChantPay.getMchid();
        String serialNumber = WChantPay.getSerialNumber();
        AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
                new WechatPay2Credentials(mchid, new PrivateKeySigner(serialNumber, merchantPrivateKey)),
                apiv3.getBytes(StandardCharsets.UTF_8));
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(WChantPay.getMchid(), serialNumber, merchantPrivateKey)
                .withValidator(new WechatPay2Validator(verifier));
        return builder.build();
    }

2、调用微信统一下单接口

代码如下(示例):

**
     * 微信统一下单
     * @param User 用户信息
     * @param wxpayDetail 后端自己生成的订单信息
     * @param httpClient 上一步生成的微信链接对象
     * @return
     * @throws Exception
     */
public Map<String,String> pay(User user,WxpayDetail wxpayDetail,HttpClient httpClient){
        HttpPost httpPost = new HttpPost(WChantPay.getPayUrl());
        httpPost.addHeader("Accept", "application/json");
        httpPost.addHeader("Content-type","application/json; charset=utf-8");
        org.apache.commons.io.output.ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectMapper objectMapper = new ObjectMapper();
        ObjectNode rootNode = objectMapper.createObjectNode();
        //商户id
        rootNode.put("mchid",WChantPay.getMchid())
                //小程序id
                .put("appid", WChantPay.getAppId())
                //描述
                .put("description", "商品描述")
                //微信通知回调地址
                .put("notify_url", WChantPay.getNotifyUrl())
                //商户订单id
                .put("out_trade_no", wxpayDetail.getHomeOrderId());
                //如果前端直接传的是分此处不需要再转
        int round = Math.round(wxpayDetail.getTotal() * 100);
        rootNode.putObject("amount")
                //支付金额,单位是(分)
                .put("total", round);
        rootNode.putObject("payer")
                //支付用户的openid
                .put("openid", merchantUsers.getOpenId());
        objectMapper.writeValue(bos, rootNode);
        httpPost.setEntity(new StringEntity(bos.toString("UTF-8")));
        HttpResponse response = httpClient.execute(httpPost);
        String bodyAsString = EntityUtils.toString(response.getEntity());
        //微信返回的预支付id
        JSONObject jsonObject = JSONObject.parseObject(bodyAsString);
    }

3、后端生成签名

/**
     *  构建签名
     * @param prepay_id  微信预订单返回的预支付订单id
     * @param orderId  商户自己的订单号
     * @return  将这个数据返回给小程序前端
     */
    public Map<String,String> responseSign(String prepay_id,String orderId) {
        Map<String,String> map=new HashMap();
        map.put("appId",WChantPay.getAppId());
        long timeStamp = System.currentTimeMillis()/1000;
        map.put("timeStamp",timeStamp+"");
        String nonceStr = WXPayUtil.generateNonceStr();
        map.put("nonceStr", nonceStr);
        map.put("package", "prepay_id="+ prepay_id);
        String 
		//一定要注意,最后也要加个\n     
		s=WChantPay.getAppId()+"\n"+timeStamp+"\n"+nonceStr+"\n"+"prepay_id="+prepay_id.toString()+"\n";
        String paySign = WXPayUtil.sign(s.getBytes(StandardCharsets.UTF_8));
        map.put("paySign",paySign);
        map.put("signType","RSA");
        map.put("orderId",orderId);
        return map;
    }

到此第一步功能就完成了,接下来就是小程序使用后端返回的数据调用微信的支付接口 具体文档参见:小程序调起支付

三、微信回调通知

微信再收到小程序支付后,会将支付结果异步通知给商户的后端,通知地址为预下单时 notify_url 字段,参见上面的统一下单代码。

接收通知这个接口我们分为两个步骤,第一个是进行验签 第二个是进行数据解密

官方文档:微信支付通知

1.微信验签

//微信验签
public boolean notify(HttpServletRequest request){
	//获取微信返回的body中的数据
   String body= ReadAsChars(request);
   //证书序列号
   String serial = request.getHeader("Wechatpay-Serial");
   //微信应答签名
   String signature= request.getHeader("Wechatpay-Signature");
   //时间戳
   String timesTamp = request.getHeader("Wechatpay-Timestamp");
   //随机字符串
   String nonce = request.getHeader("Wechatpay-Nonce");
   //初始化微信连接 参数与上面初始化微信连接 一致
   PrivateKey merchantPrivateKey = PemUtil
                .loadPrivateKey(new ByteArrayInputStream(WChantPayService.privateKey.getBytes(StandardCharsets.UTF_8)));
   AutoUpdateCertificatesVerifier autoUpdateCertificatesVerifier = new AutoUpdateCertificatesVerifier(
                new WechatPay2Credentials(WChantPay.getMchid(), new PrivateKeySigner(WChantPay.getSerialNumber(), merchantPrivateKey)),
                WChantPay.getApiv3().getBytes(StandardCharsets.UTF_8));
    ]//构建验签串
	String sign = wChantPayService.responseSign(timesTamp, nonce, body);
    System.out.println("签名串:" + sign);
    //验签 
    boolean verify = autoUpdateCertificatesVerifier.verify(serial, sign.getBytes(StandardCharsets.UTF_8), signature);
    System.out.println("验签:" + verify);
}
/**
 * 获取request中的请求body
 */
public static String ReadAsChars(HttpServletRequest request) {
        BufferedReader br = null;
        StringBuilder sb = new StringBuilder("");
        try {
            br = request.getReader();
            String str;
            while ((str = br.readLine()) != null) {
                sb.append(str);
            }
            br.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != br) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return sb.toString();
    }
 /**
     * 构造验签名串.
     *
     * @param wechatpayTimestamp HTTP头 Wechatpay-Timestamp 中的应答时间戳。
     * @param wechatpayNonce     HTTP头 Wechatpay-Nonce 中的应答随机串
     * @param body               响应体
     * @return the string
     */
    public String responseSign(String wechatpayTimestamp, String wechatpayNonce, String body) {
        return Stream.of(wechatpayTimestamp, wechatpayNonce, body)
                .collect(Collectors.joining("\n", "", "\n"));
    }

2、数据解密

收到微信的数据后,将数据记得转换成json 或者自己定义的对象,取出你需要的数据,比较简单这里就不再贴代码了

public void decodeData(String associatedData,String nonce,String ciphertext) {
 		//微信官方提供的解密工具
        AesUtil aesUtil=new AesUtil(WChantPay.getApiv3().getBytes(StandardCharsets.UTF_8));
        String rsData = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext);
    }
package com.home.machine.pay.utils;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

/**
 * @author wasin
 * @description: date 2021-05-08
 */
public class AesUtil {
    static final int KEY_LENGTH_BYTE = 32;
    static final int TAG_LENGTH_BIT = 128;
    private final byte[] aesKey;

    public AesUtil(byte[] key) {
        if (key.length != KEY_LENGTH_BYTE) {
            throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
        }
        this.aesKey = key;
    }

    /**
     *
     * @param associatedData 微信通知返回的附加数据
     * @param nonce 微信通知返回的随机字符串
     * @param ciphertext  待解密的数据
     * @return
     * @throws GeneralSecurityException
     * @throws IOException
     */
    public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext)
            throws GeneralSecurityException, IOException {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

            SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
            GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);

            cipher.init(Cipher.DECRYPT_MODE, key, spec);
            cipher.updateAAD(associatedData);

            byte[] decode = Base64.getDecoder().decode(ciphertext);
            return new String(cipher.doFinal(decode), "utf-8");
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new IllegalStateException(e);
        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
            throw new IllegalArgumentException(e);
        }
    }
}

WXPayUtil相关代码

public static String sign(byte[] message){
        Signature sign = null;
        try {
            sign = Signature.getInstance("SHA256withRSA");
            PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new 
            FileInputStream(WChantPayConfig.getPrivateKeyUrl()));//商户私钥地址 F:/xxx/xxx/apiclient_key.pem
            sign.initSign(merchantPrivateKey);
            sign.update(message);

            return Base64.getEncoder().encodeToString(sign.sign());
        } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException | FileNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }

总结

至此微信的支付就完成了,欢迎大家一起交流。