簽名驗證
商戶可以按照下述步驟驗證應答或者回調的簽名。
如果驗證商戶的請求簽名正確,微信支付會在應答的HTTP頭部中包括應答簽名。我們建議商戶驗證應答簽名。
同樣的,微信支付會在回調的HTTP頭部中包括回調報文的簽名。商戶必須 驗證回調的簽名,以確保回調是由微信支付發送。
獲取平台證書
微信支付API v3使用微信支付 的平台私鑰(不是商戶私鑰 )進行應答簽名。相應的,商戶的技術人員應使用微信支付平台證書中的公鑰驗簽。目前平台證書只提供API進行下載,請參考 獲取平台證書列表。
檢查平台證書序列號
微信支付的平台證書序列號位於HTTP頭Wechatpay-Serial
。驗證簽名前,請商戶先檢查序列號是否跟商戶當前所持有的 微信支付平台證書的序列號一致。如果不一致,請重新獲取證書。否則,簽名的私鑰和證書不匹配,將無法成功驗證簽名。
構造驗簽名串
首先,商戶先從應答中獲取以下信息。
- HTTP頭
Wechatpay-Timestamp
中的應答時間戳。 - HTTP頭
Wechatpay-Nonce
中的應答隨機串 - 應答主體(response Body)
然后,請按照以下規則構造應答的驗簽名串。簽名串共有三行,行尾以\n
結束,包括最后一行。\n
為換行符(ASCII編碼值為0x0A)。若應答報文主體為空(如HTTP狀態碼為204 No Content
),最后一行僅為一個\n
換行符。
應答時間戳\n
應答隨機串\n
應答報文主體\n
如某個應答的HTTP報文為(省略了ciphertext的具體內容):
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 {"data":[{"serial_no":"5157F09EFDC096DE15EBE81A47057A7232F1B8E1","effective_time":"2018-03-26T11:39:50+08:00","expire_time":"2023-03-25T11:39:50+08:00","encrypt_certificate":{"algorithm":"AEAD_AES_256_GCM","nonce":"4de73afd28b6","associated_data":"certificate","ciphertext":"..."}}]}
則驗簽名串為
1554209980 c5ac7061fccab6bf3e254dcf98995b8c {"data":[{"serial_no":"5157F09EFDC096DE15EBE81A47057A7232F1B8E1","effective_time":"2018-03-26T11:39:50+08:00","expire_time":"2023-03-25T11:39:50+08:00","encrypt_certificate":{"algorithm":"AEAD_AES_256_GCM","nonce":"4de73afd28b6","associated_data":"certificate","ciphertext":"..."}}]}
獲取應答簽名
微信支付的應答簽名通過HTTP頭Wechatpay-Signature
傳遞。(注意,示例因為排版可能存在換行,實際數據應在一行)
Wechatpay-Signature: CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==
對 Wechatpay-Signature
的字段值使用Base64進行解碼,得到應答簽名。
驗證簽名
很多編程語言的簽名驗證函數支持對驗簽名串和簽名 進行簽名驗證。強烈建議商戶調用該類函數,使用微信支付平台公鑰對驗簽名串和簽名進行SHA256 with RSA簽名驗證。
下面展示使用命令行演示如何進行驗簽。假設我們已經獲取了平台證書並保存為1900009191_wxp_cert.pem
。
首先,從微信支付平台證書導出微信支付平台公鑰
$ openssl x509 -in 1900009191_wxp_cert.pem -pubkey -noout > 1900009191_wxp_pub.pem $ cat 1900009191_wxp_pub.pem -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4zej1cqugGQtVSY2Ah8R MCKcr2UpZ8Npo+5Ja9xpFPYkWHaF1Gjrn3d5kcwAFuHHcfdc3yxDYx6+9grvJnCA 2zQzWjzVRa3BJ5LTMj6yqvhEmtvjO9D1xbFTA2m3kyjxlaIar/RYHZSslT4VmjIa tW9KJCDKkwpM6x/RIWL8wwfFwgz2q3Zcrff1y72nB8p8P12ndH7GSLoY6d2Tv0OB 2+We2Kyy2+QzfGXOmLp7UK/pFQjJjzhSf9jxaWJXYKIBxpGlddbRZj9PqvFPTiep 8rvfKGNZF9Q6QaMYTpTp/uKQ3YvpDlyeQlYe4rRFauH3mOE6j56QlYQWivknDX9V rwIDAQAB -----END PUBLIC KEY-----
然后,把簽名base64解碼后保存為文件signature.txt
$ openssl base64 -d -A <<< \ 'CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==' > signature.txt
最后,驗證簽名
$ openssl dgst -sha256 -verify 1900009191_wxp_pub.pem -signature signature.txt << EOF 1554209980 c5ac7061fccab6bf3e254dcf98995b8c {"data":[{"serial_no":"5157F09EFDC096DE15EBE81A47057A7232F1B8E1","effective_time":"2018-03-26T11:39:50+08:00","expire_time":"2023-03-25T11:39:50+08:00","encrypt_certificate":{"algorithm":"AEAD_AES_256_GCM","nonce":"d215b0511e9c","associated_data":"certificate","ciphertext":"..."}}]} EOF Verified OK
代碼實現
/** * 驗證簽名 * * @param timestamp 微信平台傳入的時間戳 * @param nonce 微信平台傳入的隨機字符串 * @param requestBody 微信平台傳入的消息體 * @param signature 微信平台傳入的簽名 * @return * @throws NoSuchAlgorithmException * @throws SignatureException * @throws IOException * @throws InvalidKeyException */ public static boolean signCheck(String timestamp, String nonce, Map<String, Object> requestBody, String signature) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException { //構造驗簽名串 String signatureStr = timestamp + "\n" + nonce + "\n" + JSONObject.toJSONString(requestBody) + "\n"; // 加載SHA256withRSA簽名器 Signature signer = Signature.getInstance("SHA256withRSA"); // 用微信平台公鑰對簽名器進行初始化(調上一節中的獲取平台證書方法) signer.initVerify(CommonUtils.getCertificates()); // 把我們構造的驗簽名串更新到簽名器中 signer.update(signatureStr.getBytes(StandardCharsets.UTF_8)); // 把請求頭中微信服務器返回的簽名用Base64解碼 並使用簽名器進行驗證 boolean result = signer.verify(Base64Utils.decodeFromString(signature)); return result; }
最后來個完整實例:
/** * 回調地址 * * @param requestBody * @return * @throws IOException */ @PostMapping("/api/notify/complaintsNotify") public Map<String, String> complaintsNotify(HttpServletRequest request, @RequestBody Map<String, Object> requestBody) throws Exception { Map<String, String> data = new HashMap<>(); String signature = request.getHeader("Wechatpay-Signature"); String timestamp = request.getHeader("Wechatpay-Timestamp"); String nonce = request.getHeader("Wechatpay-Nonce"); //平台證書序列號不是API證書序列號 String serial = request.getHeader("Wechatpay-Serial"); log.info("頭信息---簽名:" + signature); log.info("頭信息---時間戳:" + timestamp); log.info("頭信息---隨機字符:" + nonce); log.info("頭信息---平台證書序列號:" + serial); log.info("獲取到的body信息:" + JSONObject.toJSONString(requestBody)); //驗簽 boolean signCheck = SignUtils.signCheck(timestamp, nonce, requestBody, signature); log.info("驗簽結果:" + signCheck); if (signCheck) { //解密參數 Resource resource = JSONObject.parseObject(JSONObject.toJSONString(requestBody.get("resource")), Resource.class); AesUtil aesUtil = new AesUtil(CommonParameters.apiV3Key.getBytes("utf-8")); String string = aesUtil.decryptToString(resource.getAssociated_data().getBytes("utf-8"), resource.getNonce().getBytes("utf-8"), resource.getCiphertext()); ComplaintInfo complaintInfo = JSONObject.parseObject(string, ComplaintInfo.class); //獲取投訴詳情 ComplaintDetail complaintDetail = CommonUtils.GetComplaintsInfo(complaintInfo.getComplaint_id()); data.put("code", "SUCCESS"); data.put("message", "成功"); return data; } return data; }