簽名驗證
商戶可以按照下述步驟驗證應答或者回調的簽名。
商戶可以按照下述步驟驗證應答或者回調的簽名。
如果驗證商戶的請求簽名正確,微信支付會在應答的HTTP頭部中包括應答簽名。我們建議商戶驗證應答簽名。
同樣的,微信支付會在回調的HTTP頭部中包括回調報文的簽名。商戶必須 驗證回調的簽名,以確保回調是由微信支付發(fā)送。
微信支付API V3使用微信支付 的平臺私鑰(不是商戶私鑰 )進行應答簽名。相應的,商戶的技術人員應使用微信支付平臺證書中的公鑰驗簽。目前平臺證書只提供API進行下載,請參考 獲取平臺證書列表。
微信支付的平臺證書序列號位于HTTP頭Wechatpay-Serial
。驗證簽名前,請商戶先檢查序列號是否跟商戶當前所持有的 微信支付平臺證書的序列號一致。如果不一致,請重新獲取證書。否則,簽名的私鑰和證書不匹配,將無法成功驗證簽名。
首先,商戶先從應答中獲取以下信息。
? HTTP頭Wechatpay-Timestamp
中的應答時間戳。
? HTTP頭Wechatpay-Nonce
中的應答隨機串。
? 應答主體(response Body),需要按照接口返回的順序進行驗簽,錯誤的順序將導致驗簽失敗。
然后,請按照以下規(guī)則構造應答的驗簽名串。簽名串共有三行,行尾以\n
結束,包括最后一行。\n
為換行符(ASCII編碼值為0x0A)。若應答報文主體為空(如HTTP狀態(tài)碼為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
傳遞。(注意,示例因為排版可能存在換行,實際數(shù)據(jù)應在一行)
Wechatpay-Signature: CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==
具體組成為:
對 Wechatpay-Signature
的字段值使用Base64進行解碼,得到應答簽名。
很多編程語言的簽名驗證函數(shù)支持對驗簽名串和簽名 進行簽名驗證。強烈建議商戶調用該類函數(shù),使用微信支付平臺公鑰對驗簽名串和簽名進行SHA256 with RSA簽名驗證。
下面展示使用命令行演示如何進行驗簽。假設我們已經(jīng)獲取了平臺證書并保存為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
package com.wechat.v3;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils;
import org.junit.Test;
import javax.security.auth.login.CredentialException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.*;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
public class Verifier {
private static int RESPONSE_EXPIRED_MINUTES = 5;
private static final String certificate = "-----BEGIN CERTIFICATE-----" +
"-----END CERTIFICATE-----";
protected static IllegalArgumentException parameterError(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("parameter error: " + message);
}
//
public final boolean validate(CloseableHttpResponse response) throws IOException {
validateParameters(response);
String message = buildMessage(response);
String serial = response.getFirstHeader(WECHAT_PAY_SERIAL).getValue(); //Should be used to find which cert should be used to verify
String signature = response.getFirstHeader(WECHAT_PAY_SIGNATURE).getValue();
return verify(loadCertificate(certificate), message.getBytes(StandardCharsets.UTF_8), signature);
}
public X509Certificate loadCertificate(String certificate) {
InputStream certStream = new ByteArrayInputStream(certificate.getBytes());
X509Certificate cert = null;
try{
CertificateFactory cf = CertificateFactory.getInstance("X509");
cert = (X509Certificate) cf.generateCertificate(certStream);
cert.checkValidity();
} catch (CertificateException e) {
throw new RuntimeException("Fail to load and vailid the certificate", e);
}
return cert;
}
protected boolean verify(X509Certificate certificate, byte[] message, String signature) {
try {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initVerify(certificate);
sign.update(message);
return sign.verify(Base64.getDecoder().decode(signature));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("當前Java環(huán)境不支持SHA256withRSA", e);
} catch (SignatureException e) {
throw new RuntimeException("簽名驗證過程發(fā)生了錯誤", e);
} catch (InvalidKeyException e) {
throw new RuntimeException("無效的證書", e);
}
}
protected final void validateParameters(CloseableHttpResponse response) {
Header firstHeader = response.getFirstHeader(REQUEST_ID);
if (firstHeader == null) {
throw parameterError("empty " + REQUEST_ID);
}
String requestId = firstHeader.getValue();
// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
Header header = null;
for (String headerName : headers) {
header = response.getFirstHeader(headerName);
if (header == null) {
throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
}
}
String timestampStr = header.getValue();
try {
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
// 拒絕過期應答
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
}
} catch (DateTimeException | NumberFormatException e) {
throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
}
}
protected final String buildMessage(CloseableHttpResponse response) throws IOException {
String timestamp = response.getFirstHeader(WECHAT_PAY_TIMESTAMP).getValue();
String nonce = response.getFirstHeader(WECHAT_PAY_NONCE).getValue();
String body = getResponseBody(response);
return timestamp + "\n"
+ nonce + "\n"
+ body + "\n";
}
protected final String getResponseBody(CloseableHttpResponse response) throws IOException {
HttpEntity entity = response.getEntity();
return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
}
}
Customer Service Tel
Business Development
9:00-18:00
Monday-Friday GMT+8
Technical Support
WeChat Pay Global
ICP證