> 文档中心 > 微信支付V3版java整合版让你不再无从下手

微信支付V3版java整合版让你不再无从下手

对于微信支付V3版本,相信大家都不陌生,官方文档很笼统,demo也是无从下手,需要先过一遍demo源码,理解业务,才能进行下一步的开发,并且中间坑也不少,许多刚接触的人,都会感觉无从下手的感觉。所以今天出一版java接入微信支付V3版本整合版。

对于安全基础设施部分的工作,初次接入的小伙伴恐怕会是无从下手,那就介绍一下,微信支付V3的安全验签和签名登操作。

微信支付V3版java整合版

    • 1、引入依赖:
    • 2、解密工具类
    • 3、支付接口枚举类
    • 4、对外暴露方法
      • 4、1支付下单 V3Pay
      • 4.2 、微信调起支付参数 WxTuneUp()
      • 4.3、处理微信异步回调 notify()
    • 5、安全验证类
      • 5.1、生成组装请求头 getToken()
      • 5.2、生成签名 sign()
      • 5.3、组装签名加载 buildMessage()
      • 5.4、获取私钥 getPrivateKey()
      • 5.5、构造签名串 buildSignMessage()
      • 5.6、v3支付异步通知验证签名 verifyNotify()
      • 5.7、处理返回对象 readData()

1、引入依赖:

    com.github.wechatpay-apiv3    wechatpay-apache-httpclient    0.4.7

2、解密工具类

public class AesUtil {    static final int KEY_LENGTH_BYTE = 32;    static final int TAG_LENGTH_BIT = 128;    private final byte[] aesKey;    /**     * @param key APIv3 密钥     */    public AesUtil(byte[] key) { if (key.length != KEY_LENGTH_BYTE) {     throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节"); } this.aesKey = key;    }    /**     * 证书和回调报文解密     *     * @param associatedData associated_data     * @param nonce   nonce     * @param cipherText     ciphertext     * @return {String} 平台证书明文     * @throws GeneralSecurityException 异常     */    public String decryptToString(byte[] associatedData, byte[] nonce, String cipherText) throws GeneralSecurityException { 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);     return new String(cipher.doFinal(Base64.decode(cipherText)), StandardCharsets.UTF_8); } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {     throw new IllegalStateException(e); } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {     throw new IllegalArgumentException(e); }    }}

3、支付接口枚举类

public enum WxPayEnum {    //app支付    APP_PAY("v3/pay/transactions/app"),    //查询订单    QUERY_PAY("v3/pay/transactions/id/"),    //关闭订单    CLOSE_PAY("v3/pay/transactions/out-trade-no/{out_trade_no}/close"),    //申请退款    REFUNDS_PAY("v3/refund/domestic/refunds"),    //查询单笔退款    QUERY_REFUNDS_PAY("v3/refund/domestic/refunds/"),    //申请交易账单    BILL_PAY("v3/bill/tradebill"),    //申请资金账单    FUNDFLOWBILL_PAY("v3/bill/fundflowbill");    private String desc;    WxPayEnum(String desc) { this.desc = desc;    }    public String getDesc() { return desc;    }}

4、对外暴露方法

4、1支付下单 V3Pay

public class WxPayGet {    //请求网关    private static final String url_prex = "https://api.mch.weixin.qq.com/";    //APP支付    private static final String url = WxPayEnum.APP_PAY.getDesc();    //编码    private static final String charset = "UTF-8";    public static String WxPayGet(String jsonStr, String merchId, String serial_no, String privateKeyFilePath) throws Exception { String body = ""; CloseableHttpClient client = HttpClients.createDefault(); HttpPost post = new HttpPost(url_prex + url); StringEntity s = new StringEntity(jsonStr, charset); s.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE, "application/json")); //设置参数到请求体中去 post.setEntity(s); String token = new GetToken().getToken("POST", HttpUrl.parse(url_prex + url), merchId, serial_no, privateKeyFilePath, jsonStr); post.setHeader("Content-Type", "application/json"); post.setHeader("Accept", "application/json"); post.setHeader("Authorization",  "WECHATPAY2-SHA256-RSA2048 " + token); //执行请求操作,并拿到结果(同步阻塞) CloseableHttpResponse response = client.execute(post); HttpEntity entity = response.getEntity(); if (entity != null) {     //按指定编码转换结果实体为String类型     body = EntityUtils.toString(entity, charset); } EntityUtils.consume(entity); response.close(); return JSON.parseObject(body).get("prepay_id").toString();    }}

4.2 、微信调起支付参数 WxTuneUp()

/**     * 微信调起支付参数     * 返回参数如有不理解 请访问微信官方文档     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_1_4.shtml     *     * @param prepayId    微信下单返回的prepay_id     * @param appId应用ID(appid)     * @param privateKeyFilePath 私钥的地址     * @return 当前调起支付所需的参数     * @throws Exception     */    public static JSONObject WxTuneUp(String prepayId, String appId, String privateKeyFilePath) throws Exception { String time = System.currentTimeMillis() / 1000 + ""; String nonceStr = UUID.randomUUID().toString().replace("-", ""); String packageStr = "prepay_id=" + prepayId; ArrayList<String> list = new ArrayList<>(); list.add(appId); list.add(time); list.add(nonceStr); list.add(packageStr); //加载签名 String packageSign = sign(buildSignMessage(list).getBytes(), privateKeyFilePath); JSONObject jsonObject = new JSONObject(); jsonObject.put("appid", appId); jsonObject.put("timeStamp", time); jsonObject.put("nonceStr", nonceStr); jsonObject.put("packages", packageStr); jsonObject.put("signType", "RSA"); jsonObject.put("paySign", packageSign); return jsonObject;    }

4.3、处理微信异步回调 notify()

/**     * 处理微信异步回调     *     * @param request     * @param response     * @param privateKey 32的秘钥     */    public static String notify(HttpServletRequest request, HttpServletResponse response, String privateKey) throws Exception { Map<String, String> map = new HashMap<>(12); String result = readData(request); // 需要通过证书序列号查找对应的证书,verifyNotify 中有验证证书的序列号 String plainText = verifyNotify(result, privateKey); if (StrUtil.isNotEmpty(plainText)) {     response.setStatus(200);     map.put("code", "SUCCESS");     map.put("message", "SUCCESS"); } else {     response.setStatus(500);     map.put("code", "ERROR");     map.put("message", "签名错误"); } response.setHeader("Content-type", ContentType.JSON.toString()); response.getOutputStream().write(JSONUtil.toJsonStr(map).getBytes(StandardCharsets.UTF_8)); response.flushBuffer(); String out_trade_no = JSONObject.fromObject(plainText).getString("out_trade_no"); return out_trade_no;    }

5、安全验证类

5.1、生成组装请求头 getToken()

  //请求头token    public String getToken(String method, HttpUrl url, String merchId, String serial_no, String privateKeyFilePath, String body) throws Exception { String nonceStr = generateNonceStr(); long timestamp = System.currentTimeMillis() / 1000; String message = buildMessage(method, url, timestamp, nonceStr, body); String signature = sign(message.getBytes("UTF-8"), privateKeyFilePath); String token = "merchId=\"" + merchId + "\","  + "nonce_str=\"" + nonceStr + "\","  + "timestamp=\"" + timestamp + "\","  + "serial_no=\"" + serial_no + "\","  + "signature=\"" + signature + "\""; log.info("authorization token=[{}]", token); return token;    }

5.2、生成签名 sign()

 //生成签名    static String sign(byte[] message, String privateKeyFilePath) { try {     Signature sign = Signature.getInstance("SHA256withRSA");     sign.initSign(getPrivateKey(privateKeyFilePath));     sign.update(message);     return Base64.getEncoder().encodeToString(sign.sign()); } catch (NoSuchAlgorithmException e) {     throw new RuntimeException("当前Java环境不支持SHA256withRSA", e); } catch (SignatureException | IOException e) {     throw new RuntimeException("签名验证过程发生了错误", e); } catch (InvalidKeyException e) {     throw new RuntimeException("无效的证书", e); }    }

5.3、组装签名加载 buildMessage()

 //组装签名加载 buildMessage    static String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) { String canonicalUrl = url.encodedPath(); if (url.encodedQuery() != null) {     canonicalUrl += "?" + url.encodedQuery(); } return method + "\n"  + canonicalUrl + "\n"  + timestamp + "\n"  + nonceStr + "\n"  + body + "\n";    }

5.4、获取私钥 getPrivateKey()

    //获取私钥public static PrivateKey getPrivateKey(String filename) throws IOException { String content = new String(Files.readAllBytes(Paths.get(filename)), "UTF-8"); try {     String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")      .replace("-----END PRIVATE KEY-----", "")      .replaceAll("\\s+", "");     KeyFactory kf = KeyFactory.getInstance("RSA");     return kf.generatePrivate(      new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))); } catch (NoSuchAlgorithmException e) {     throw new RuntimeException("当前Java环境不支持RSA", e); } catch (InvalidKeySpecException e) {     throw new RuntimeException("无效的密钥格式"); }    }

5.5、构造签名串 buildSignMessage()

/**     * 构造签名串     *     * @param signMessage 待签名的参数     * @return 构造后带待签名串     */    static String buildSignMessage(ArrayList<String> signMessage) { if (signMessage == null || signMessage.size() <= 0) {     return null; } StringBuilder sbf = new StringBuilder(); for (String str : signMessage) {     sbf.append(str).append("\n"); } return sbf.toString();    }

5.6、v3支付异步通知验证签名 verifyNotify()

/**     * v3 支付异步通知验证签名     *     * @param body 异步通知密文     * @param key  api 密钥     * @return 异步通知明文     * @throws Exception 异常信息     */    static String verifyNotify(String body, String key) throws Exception { // 获取平台证书序列号 cn.hutool.json.JSONObject resultObject = JSONUtil.parseObj(body); cn.hutool.json.JSONObject resource = resultObject.getJSONObject("resource"); String cipherText = resource.getStr("ciphertext"); String nonceStr = resource.getStr("nonce"); String associatedData = resource.getStr("associated_data"); AesUtil aesUtil = new AesUtil(key.getBytes(StandardCharsets.UTF_8)); // 密文解密 return aesUtil.decryptToString(  associatedData.getBytes(StandardCharsets.UTF_8),  nonceStr.getBytes(StandardCharsets.UTF_8),  cipherText );    }

5.7、处理返回对象 readData()

/**     * 处理返回对象     *     * @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();  }     } }    }

最后就是业务上的接口调用了,比如下单、退单,等情况。