1. 背景介绍

v3版微信支付通过商户证书和平台证书加强了安全性,java版sdk包wechatpay-apache-httpclient内部封装了安全性相关的签名、验签、加密和解密工作,降低了开发难度。下面几个特性的实现,更方便了开发者。

  1. 平台证书自动更新,无需开发者关注平台证书有效性,无需手动下载更新;
  2. 执行请求时将自动携带身份认证信息,并检查应答的微信支付签名。

如果文档中有错误的地方,需要路过的大佬指出,我会尽快更改。跪谢。。。

2. API证书

2.1 API 密钥设置( 一定要设置的!!!! )

请登录商户平台进入【账户中心】->【账户设置】->【API安全】->【APIv3密钥】中设置 API 密钥。

具体操作步骤请参见:什么是APIv3密钥?如何设置?

2.2 获取 API 证书

请登录商户平台进入【账户中心】->【账户设置】->【API安全】根据提示指引下载证书。

具体操作步骤请参见:什么是API证书?如何获取API证书?

简单叙述:(详细信息请看官方文档

证书 描述 备注
apiclient_cert.p12 包含了私钥信息的证书文件 是商户证书文件(双向证书)
apiclient_cert.pem 从apiclient_cert.p12中导出证书部分的文件,为pem格式 简单理解:公钥
apiclient_key.pem 从apiclient_key.pem中导出密钥部分的文件 简单理解:私钥

3. 创建client(请参照官方文档

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.util.PemUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;

@Slf4j
@Aspect
@Component
public class WxPayHttpClientFactory {

    public static CloseableHttpClient httpClient;
    public static Verifier verifier;

    @Before("execution(public * com.xxx.admin.web.controller.xxx.*Controller.*(..))" +
            "||execution(public * com.xxx.admin.web.controller.xxx.*Controller.*(..))")
    public void initWXPayClient() {
        try {
            // 加载商户私钥(privateKey:私钥字符串)
            PrivateKey merchantPrivateKey = PemUtil
                    .loadPrivateKey(new ClassPathResource("apiclient_key.pem classpath路径").getInputStream());

            X509Certificate certificate = PemUtil.loadCertificate(new ClassPathResource("apiclient_cert.pem classpath路径").getInputStream());
            String serialNo = certificate.getSerialNumber().toString(16).toUpperCase();
            //merchantId:商户号,serialNo:商户证书序列号
            // 获取证书管理器实例
            CertificatesManager certificatesManager = CertificatesManager.getInstance();
            // 向证书管理器增加需要自动更新平台证书的商户信息
            certificatesManager.putMerchant("商户号", new WechatPay2Credentials("商户号",
                    new PrivateKeySigner(serialNo, merchantPrivateKey)), wechatAppPayConfig.api_v3.getBytes(StandardCharsets.UTF_8));
            // 从证书管理器中获取verifier
            //版本>=0.4.0可使用 CertificatesManager.getVerifier(mchId) 得到的验签器替代默认的验签器。
            // 它会定时下载和更新商户对应的微信支付平台证书 (默认下载间隔为UPDATE_INTERVAL_MINUTE)。
            verifier = certificatesManager.getVerifier("商户号");

            //创建一个httpClient
            httpClient = WechatPayHttpClientBuilder.create()
                    .withMerchant("商户号", serialNo, merchantPrivateKey)
                    .withValidator(new WechatPay2Validator(verifier)).build();
        } catch (IOException e) {
            e.printStackTrace();
            log.error("加载秘钥文件失败");
        } catch (GeneralSecurityException e) {
            e.printStackTrace();
            log.error("获取平台证书失败");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @After("execution(public * com.xxx.admin.web.controller.xxx.*Controller.*(..))" +
            "||execution(public * com.xxx.admin.web.controller.xxx.*Controller.*(..))")
    public void closeWXClient() {
        if (httpClient != null) {
            try {
                httpClient.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

官方文档指出 自动携带身份认证信息,并检查应答的微信支付签名 。所以上面的代码就没携带任何签名。

1648619975768.png

4. 生成APP支付订单(请参照官方文档

    /**
     * 微信支付POST请求
     *
     * @param reqUrl       请求地址 示例:https://api.mch.weixin.qq.com/v3/pay/transactions/app
     * @param paramJsonStr 请求体 json字符串 此参数与微信官方文档一致
     * @return 订单支付的参数
     * @throws Exception
     */
    public static String V3PayPost(String reqUrl, String paramJsonStr) throws Exception {
        //创建post方式请求对象
        HttpPost httpPost = new HttpPost(reqUrl);
        //装填参数
        StringEntity s = new StringEntity(paramJsonStr, "utf-8");
        s.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE,
                "application/json"));
        //设置参数到请求对象中
        httpPost.setEntity(s);
        //指定报文头【Content-type】、【User-Agent】
        httpPost.setHeader("Content-type", "application/json");
        httpPost.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36");
        httpPost.setHeader("Accept", "application/json");
        //执行请求操作,并拿到结果(同步阻塞)
        CloseableHttpResponse response = WxPayHttpClientFactory.httpClient.execute(httpPost);
        int statusCode = response.getStatusLine().getStatusCode();
        //获取数据,并释放资源
        String body = closeHttpResponse(response);
        if (statusCode == 200) { //处理成功
            switch (reqUrl) {
                case "https://api.mch.weixin.qq.com/v3/pay/transactions/app"://返回APP支付所需的参数
                    return JSONObject.parseObject(body).getString("prepay_id");
                case "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds"://返回APP退款结果
                    return body;
            }
        }
        return null;
    }
    /**
     * 获取数据,并释放资源
     *
     * @param response
     * @return
     * @throws IOException
     */
    public static String closeHttpResponse(CloseableHttpResponse response) throws IOException {
        String body = "";
        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode == 200) { //处理成功
            //获取结果实体
            HttpEntity entity = response.getEntity();
            if (entity != null) {
                //按指定编码转换结果实体为String类型
                body = EntityUtils.toString(entity, "utf-8");
            }
            //EntityUtils.consume将会释放所有由httpEntity所持有的资源
            EntityUtils.consume(entity);
        }
        //释放链接
        response.close();
        return body;
    }

至此就成功 在微信支付服务后台生成预支付交易单,并且拿到prepay_id,但是调起APP支付,还需其他字段,也就是所谓的二签。可以直接使用下面的方法WxAppPayTuneUp()

5.APP调起支付(请参照官方文档

    /**
     * 微信调起支付参数
     * 返回参数如有不理解 请访问微信官方文档
     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_2_4.shtml
     *
     * @param prepayId         微信下单返回的prepay_id
     * @param appId            应用ID
     * @param mch_id           商户号
     * @param private_key_path 私钥路径
     * @return 当前调起支付所需的参数
     * @throws Exception
     */
    public static String WxAppPayTuneUp(String prepayId, String appId, String mch_id, String private_key_path) throws Exception {
        if (StringUtils.isNotBlank(prepayId)) {
            long timestamp = System.currentTimeMillis() / 1000;
            String nonceStr = generateNonceStr();
            //加载签名
            String packageSign = sign(buildMessage(appId, timestamp, nonceStr, prepayId).getBytes(), private_key_path);
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("appId", appId);
            jsonObject.put("prepayId", prepayId);
            jsonObject.put("timeStamp", timestamp);
            jsonObject.put("nonceStr", nonceStr);
            jsonObject.put("package", "Sign=WXPay");
            jsonObject.put("signType", "RSA");
            jsonObject.put("sign", packageSign);
            jsonObject.put("partnerId", mch_id);
            return jsonObject.toJSONString();
        }
        return "";
    }
    public static String sign(byte[] message, String private_key_path) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException {
        //签名方式
        Signature sign = Signature.getInstance("SHA256withRSA");
        //私钥
        sign.initSign(PemUtil
                .loadPrivateKey(new ClassPathResource(private_key_path).getInputStream()));
        sign.update(message);

        return Base64.getEncoder().encodeToString(sign.sign());
    }
	/**
     * 按照前端签名文档规范进行排序,\n是换行
     *
     * @param appId     appId
     * @param timestamp 时间
     * @param nonceStr  随机字符串
     * @param prepay_id prepay_id
     * @return
     */
    public static String buildMessage(String appId, long timestamp, String nonceStr, String prepay_id) {
        return appId + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + prepay_id + "\n";
    }
    protected static final SecureRandom RANDOM = new SecureRandom();
	//生成随机字符串 微信底层的方法,直接copy出来了
    protected static String generateNonceStr() {
        char[] nonceChars = new char[32];
        for (int index = 0; index < nonceChars.length; ++index) {
            nonceChars[index] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".charAt(RANDOM.nextInt("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".length()));
        }
        return new String(nonceChars);
    }

将WxAppPayTuneUp()返回的字符串,响应给前端即可

6.支付通知(异步通知,请参照官方文档

    @PostMapping("/wxAppPayNotify")
    public JSONObject wxAppPayNotify(HttpServletRequest request, HttpServletResponse response) throws IOException, GeneralSecurityException {
        //从请求头获取验签字段
        String signature = request.getHeader("Wechatpay-Signature");
        String serial = request.getHeader("Wechatpay-Serial");
        ServletInputStream inputStream = request.getInputStream();
        StringBuilder sb = new StringBuilder();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        String s;
        //读取回调请求体
        while ((s = bufferedReader.readLine()) != null) {
            sb.append(s);
        }
        String s1 = sb.toString();
        //按照文档要求拼接验签串
        String verifySignature = request.getHeader("Wechatpay-Timestamp") + "\n"
                + request.getHeader("Wechatpay-Nonce") + "\n" + s1 + "\n";

        //使用官方验签工具进行验签
        boolean verify1 = WxPayHttpClientFactory.verifier.verify(serial, verifySignature.getBytes(), signature);
        //判断验签的结果
        if (!verify1) {
            //验签失败,应答接口
            //设置状态码
            response.setStatus(500);
            JSONObject jsonResponse = new JSONObject();
            jsonResponse.put("code", "FAIL");
            jsonResponse.put("message", "失败");
            return jsonResponse;
        }

        JSONObject parseObject = JSONObject.parseObject(s1);
        if ("TRANSACTION.SUCCESS".equals(parseObject.getString("event_type"))
                && "encrypt-resource".equals(parseObject.getString("resource_type"))) {
            //通知的类型,支付成功通知的类型为TRANSACTION.SUCCESS
            //通知的资源数据类型,支付成功通知为encrypt-resource
            JSONObject resourceJson = JSONObject.parseObject(parseObject.getString("resource"));
            String associated_data = resourceJson.getString("associated_data");
            String nonce = resourceJson.getString("nonce");
            String ciphertext = resourceJson.getString("ciphertext");

            //解密,如果这里报错,就一定是APIv3密钥错误
            AesUtil aesUtil = new AesUtil(api_v3.getBytes());
            String aes = aesUtil.decryptToString(associated_data.getBytes(), nonce.getBytes(), ciphertext);
		   System.out.println("解密后=" + aes);
            //dosomething 处理业务
            }
        }

        JSONObject jsonResponse = new JSONObject();
        jsonResponse.put("code", "SUCCESS");
        jsonResponse.put("message", "成功");
        //设置状态码
        response.setStatus(200);
        return jsonResponse;
    }

以上是一套APP下单的流程【APP下单】->【APP调起支付】->【微信支付异步通知】