手把手教你springboot整合微信支付

2022-08-12 18:02:02



最近要做一個微信小程式,需要微信支付,所以研究了下怎麼在 java 上整合微信支付功能,特此記錄下。

本文完整程式碼:點選跳轉

準備工作

小程式開通微信支付

  1. 首先需要在微信支付的官網點選跳轉上註冊一個服務商
  2. 在服務商的管理頁面中申請關聯小程式,通過小程式的 appid 進行關聯
  3. 進入微信公眾平臺,功能-微信支付中確認關聯(如果服務商和小程式的註冊主體不一樣,還要經過微信的稽核)

獲取各種證書、金鑰檔案

這裡比較麻煩,需要認真點。

目前微信支付的 api 有 V2 和 V3 兩個版本,V2 是 xml 的資料結構不建議用了,很麻煩(雖然 V3 也不簡單).

以下內容全部基於微信支付 V3 的版本

你需要獲取如下東西:

  • 商戶 id:這個可以在小程式微信公眾平臺-功能-微信支付 頁面中的已關聯商戶號中得到
  • 商戶金鑰:這個需要在微信支付的管理後臺中申請獲取
  • 證書編號: 同樣在微信支付的管理後臺中申請證書,申請證書後就會看到證書編號
  • 證書私鑰:上一步申請證書的時候同時也會獲取到證書的公鑰、私鑰檔案。開發中只需要私鑰檔案

程式碼開發

由於支付屬於比較敏感的操作,所以建議將引數設定放在後臺,前端請求獲取到引數後直接調起微信支付。

整個微信支付流程如下:

  1. 小程式端請求後臺獲取統一支付引數
  2. 後臺呼叫微信 api(官方檔案)生成預訂單,並構造統一下單介面的引數返回小程式
  3. 小程式根據引數呼叫統一下單介面(官方檔案)

實際開發中,小程式端的開發內容很少,98%的工作量都在後臺。

準備請求 htpClient

雖然微信官方目前沒有正式放出官方 sdk,但是 github 上已經有了 sdk 的倉庫,目前正在開發中,地址.這個工具做了一定程度的封裝,極大簡化了加密解密證書設定等繁瑣的操作。

和微信的請求需要做雙向加密,因此要在系統啟動時建立一個專用的 httpClient,用來呼叫微信支付 api.程式碼如下:

@PostConstruct
public void init() throws Exception {
    log.info("私鑰路徑:{}", certKeyPath);
    PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new FileInputStream(certKeyPath));
    // 獲取證書管理器範例
    certificatesManager = CertificatesManager.getInstance();
    sign = SecureUtil.sign(SignAlgorithm.SHA256withRSA, merchantPrivateKey.getEncoded(), null);

    // 向證書管理器增加需要自動更新平臺證書的商戶資訊
    certificatesManager.putMerchant(merchantId, new WechatPay2Credentials(merchantId,
            new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)), apiV3Key.getBytes(StandardCharsets.UTF_8));
    // 從證書管理器中獲取verifier
    Verifier verifier = certificatesManager.getVerifier(merchantId);
    WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
            .withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
            .withValidator(new WechatPay2Validator(verifier));
    httpClient = builder.build();
}

預定單生成並返回小程式請求引數

程式碼如下:

JSONObject obj = new JSONObject();
        obj.put("mchid", merchantId);
        obj.put("appid", appId);
        obj.put("description", body.getDescription());
        obj.put("out_trade_no", body.getSn());
        obj.put("notify_url", "https://backend/asdfasdf/notify");
        Map<String, String> attach = new HashMap<>();
        attach.put("sn", body.getSn());
        obj.put("attach", JSON.toJSONString(attach));
        JSONObject amount = new JSONObject();
        amount.put("total", body.getPrice().multiply(BigDecimal.valueOf(100)).intValue());
        obj.put("amount", amount);
        JSONObject payer = new JSONObject();
        //放入使用者的openId
        payer.put("openid", "");
        obj.put("payer", payer);
        log.info("請求引數為:" + JSON.toJSONString(obj));
        HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");
        httpPost.addHeader("Accept", "application/json");
        httpPost.addHeader("Content-type", "application/json; charset=utf-8");
        httpPost.setEntity(new StringEntity(obj.toJSONString(), "UTF-8"));
        try {
            CloseableHttpResponse response = httpClient.execute(httpPost);
            String bodyAsString = EntityUtils.toString(response.getEntity());
            String prePayId = JSONObject.parseObject(bodyAsString).getString("prepay_id");

            //準備小程式端的請求引數
            Map<String, String> map = new HashMap<>(6);
            map.put("appId", appId);
            String timeStamp = String.valueOf(now / 1000);
            map.put("timeStamp", timeStamp);
            String nonceStr = IdUtil.fastSimpleUUID();
            map.put("nonceStr", nonceStr);
            String packageStr = "prepay_id=" + prePayId;
            map.put("package", packageStr);
            map.put("signType", "RSA");
            // 注意\n必須新增
            map.put("paySign", Base64.encode(sign.sign(appId + "\n" + timeStamp + "\n" + nonceStr + "\n" + packageStr + "\n")));
            return map;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

注意在生成小程式請求引數 paySign 時,\n 必須新增

小程式端請求

小程式端獲取請求引數後,直接呼叫wx.requestPayment(後臺返回的引數),即可調起支付

支付成功回撥

微信支付成功後,會通知伺服器端支付成功,通過之前設定的回撥介面。注意回撥介面必須為 https。

微信回撥引數也是加密的,必須要經過解密後才能獲取,程式碼如下:

注意:部分引數是通過請求頭提供的,nginx 等代理在轉發請求時可能會將請求頭過濾掉,導致無法獲取對應引數

@Override
    public void notify(HttpServletRequest servletRequest) {

        String timeStamp = servletRequest.getHeader("Wechatpay-Timestamp");
        String nonce = servletRequest.getHeader("Wechatpay-Nonce");
        String signature = servletRequest.getHeader("Wechatpay-Signature");
        String certSn = servletRequest.getHeader("Wechatpay-Serial");

        try (BufferedReader reader = new BufferedReader(new InputStreamReader(servletRequest.getInputStream()))) {
            StringBuilder stringBuilder = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                stringBuilder.append(line);
            }
            String obj = stringBuilder.toString();
            log.info("回撥資料:{},{},{},{},{}", obj, timeStamp, nonce, signature, certSn);
            Verifier verifier = certificatesManager.getVerifier(merchantId);
            String sn = verifier.getValidCertificate().getSerialNumber().toString(16).toUpperCase(Locale.ROOT);
            NotificationRequest request = new NotificationRequest.Builder().withSerialNumber(sn)
                    .withNonce(nonce)
                    .withTimestamp(timeStamp)
                    .withSignature(signature)
                    .withBody(obj)
                    .build();
            NotificationHandler handler = new NotificationHandler(verifier, apiV3Key.getBytes(StandardCharsets.UTF_8));
            // 驗籤和解析請求體
            Notification notification = handler.parse(request);
            JSONObject res = JSON.parseObject(notification.getDecryptData());
            //做一些操作
            JSONObject attach = res.getJSONObject("attach");
        } catch (Exception e) {
            log.error("微信支付回撥失敗", e);
        }
    }

拓展

以上只有關鍵的支付和回撥介面範例,其他的介面請求也是類似的,就不一一列出。