Java中的微信支付(3):API V3對微信伺服器響應進行簽名驗證

碼農小胖哥發表於2020-11-04

1. 前言

牢記一句話:公鑰加密,私鑰解密;私鑰加簽,公鑰驗籤。

微信支付V3版本前兩篇分別講了如何對請求做簽名和如何獲取並重新整理微信平臺公鑰,本篇將繼續展開如何對微信支付響應結果的驗籤。

2. 為什麼要對響應驗籤

微信支付會在回撥的HTTP頭部中包括回撥報文的簽名。商戶必須驗證響應的簽名,保證響應確實來自微信支付伺服器,避免中間人攻擊。而驗證響應簽名除了需要微信平臺的公鑰外還需要從請求頭的其它引數。

假設以下就是微信支付伺服器的響應:

HTTP/1.1 200 OK
Server: nginx
Date: Tue, 02 Apr 2019 12:59:40 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2204
Connection: keep-alive
Keep-Alive: timeout=8
Content-Language: zh-CN
Request-ID: e2762b10-b6b9-5108-a42c-16fe2422fc8a
Wechatpay-Nonce: c5ac7061fccab6bf3e254dcf98995b8c
Wechatpay-Signature: CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==
Wechatpay-Timestamp: 1554209980
Wechatpay-Serial: 5157F09EFDC096DE15EBE81A47057A7232F1B8E1
Cache-Control: no-cache, must-revalidate

{"prepay_id":"wx2922034726858082fbd40b511c67630000"}

檢查平臺證照序列號

微信支付響應的時候會攜帶一個微信平臺證照序列號,從響應頭中的Wechatpay-Serial欄位中獲取值,用來提示我們要使用該序列號的證照來進行驗籤,如果不存在就需要我們重新整理證照,而上一文我們將平臺證照序列號和證照以鍵值對存在HashMap中,我們只需要檢查是否存在即可,不存在就重新整理。

構造驗簽名串

從響應結果中獲取對應下面方法的三個引數就可以構造出驗簽名串。

/**
 * 構造驗簽名串.
 *
 * @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"));
}

驗證簽名

待驗證的簽名從響應頭中的Wechatpay-Signature欄位中獲取,我們使用微信支付平臺公鑰對驗簽名串和簽名進行SHA256 with RSA簽名驗證。

   // 構造驗簽名串  
        final String signatureStr = responseSign(wechatpayTimestamp, wechatpayNonce, body);
   // 載入SHA256withRSA簽名器
        Signature signer = Signature.getInstance("SHA256withRSA");
  // 用微信平臺公鑰對簽名器進行初始化
        signer.initVerify(certificate);
   // 把我們構造的驗簽名串更新到簽名器中
        signer.update(signatureStr.getBytes(StandardCharsets.UTF_8));
   // 把請求頭中微信伺服器返回的簽名用Base64解碼 並使用簽名器進行驗證
        boolean result = signer.verify(Base64Utils.decodeFromString(wechatpaySignature));

完整的驗籤程式碼

/**
 * 我方對響應驗籤,和應答簽名做比較,使用微信平臺證照.
 *
 * @param wechatpaySerial    response.headers['Wechatpay-Serial']    當前使用的微信平臺證照序列號
 * @param wechatpaySignature response.headers['Wechatpay-Signature']   微信平臺簽名
 * @param wechatpayTimestamp response.headers['Wechatpay-Timestamp']  微信伺服器的時間戳
 * @param wechatpayNonce     response.headers['Wechatpay-Nonce']   微信伺服器提供的隨機串
 * @param body               response.body 微信伺服器的響應體
 * @return the boolean
 */
@SneakyThrows
public boolean responseSignVerify(String wechatpaySerial, String wechatpaySignature, String wechatpayTimestamp, String wechatpayNonce, String body) {

    if (CERTIFICATE_MAP.isEmpty() || !CERTIFICATE_MAP.containsKey(wechatpaySerial)) {
        refreshCertificate();
    }
    Certificate certificate = CERTIFICATE_MAP.get(wechatpaySerial);

    final String signatureStr = createSign(wechatpayTimestamp, wechatpayNonce, body);
    Signature signer = Signature.getInstance("SHA256withRSA");
    signer.initVerify(certificate);
    signer.update(signatureStr.getBytes(StandardCharsets.UTF_8));

    return signer.verify(Base64Utils.decodeFromString(wechatpaySignature));
}

CERTIFICATE_MAP 平臺證照容器可參考上一篇文章。

3. 總結

驗籤通過就說明我們請求的響應來自微信伺服器就可以針對結果進行對應的邏輯處理了,微信支付API無論是V2還是V3都包含了使用Api證照對請求進行加簽,對響應結果進行驗籤的流程,十分考驗對密碼摘要演算法的使用,其它就是組織引數呼叫Http請求。如果你能夠掌握這一能力就會在面試中和工作中佔到優勢。好了今天分享就到這裡,多多關注: 碼農小胖哥 獲取更多實用的程式設計乾貨。

關注公眾號:Felordcn 獲取更多資訊

個人部落格:https://felord.cn

相關文章