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

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

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

1、引入依赖:

<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-apache-httpclient</artifactId>
    <version>0.4.7</version>
</dependency>

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();
                }
            }
        }
    }

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

猜你喜欢

转载自blog.csdn.net/weixin_44427181/article/details/124943279
今日推荐