
1. 前言
在Java中的微信支付(1):API V3版本簽名詳解一文中胖哥講解了微信支付V3版本API的簽名,當(dāng)我方(你自己的服務(wù)器)請(qǐng)求微信支付服務(wù)器時(shí)需要根據(jù)我方的API證書對(duì)參數(shù)進(jìn)行加簽,微信服務(wù)器會(huì)根據(jù)我方簽名驗(yàn)簽以確定請(qǐng)求來(lái)自我方服務(wù)器。那么同樣的道理我方的服務(wù)器也要對(duì)微信支付服務(wù)器的響應(yīng)進(jìn)行鑒別來(lái)確定響應(yīng)真的來(lái)自微信支付服務(wù)器,這就是驗(yàn)簽。驗(yàn)簽使用的是【微信支付平臺(tái)證書公鑰】,不是商戶API證書。使用商戶API證書是驗(yàn)證不過(guò)的。今天就來(lái)分享一下如何獲得微信平臺(tái)公鑰和動(dòng)態(tài)刷新微信平臺(tái)公鑰。
2. 獲取微信平臺(tái)證書公鑰
微信平臺(tái)證書是微信支付平臺(tái)自己的證書,我們是管不了的,而且是有效期的。
微信服務(wù)器會(huì)定期更換,所以也要求我方定期獲取公鑰。而且我們只能通過(guò)調(diào)用接口/v3/certificates來(lái)獲得,此接口也需要進(jìn)行簽名(可參考上一篇文章)。你可以獲取證書后靜態(tài)放到服務(wù)器上,手動(dòng)更新靜態(tài)證書;也可以動(dòng)態(tài)獲取一勞永逸。本文采取一勞永逸的辦法。
平臺(tái)證書接口文檔:https://wechatpay-api.gitbook.io/wechatpay-api-v3/jie-kou-wen-dang/ping-tai-zheng-shu
3. 證書和回調(diào)報(bào)文解密
為了保證安全性,微信支付在回調(diào)通知和平臺(tái)證書下載接口中,對(duì)關(guān)鍵信息進(jìn)行了AES-256-GCM加密。也就是說(shuō)我們拿到響應(yīng)的信息是被加密的,需要解密后才能獲得真正的微信平臺(tái)證書公鑰。響應(yīng)體大致是這樣的,具體根據(jù)你調(diào)用平臺(tái)證書接口,應(yīng)該大差不差是下面這個(gè)結(jié)構(gòu):
{
"data": [
{
"effective_time": "2020-10-21T14:48:49+08:00",
"encrypt_certificate": {
// 加密算法
"algorithm": "AEAD_AES_256_GCM",
// 附加數(shù)據(jù)包(可能為空)
"associated_data": "certificate",
// Base64編碼后的密文
"ciphertext": "",
// 加密使用的隨機(jī)串初始化向量)
"nonce": "88b4e15a0db9"
},
"expire_time": "2025-10-20T14:48:49+08:00",
// 證書序列號(hào)
"serial_no": "217016F42805DD4D5442059D373F98BFC5252599"
}
]
}
你可以使用各種JSON類庫(kù)取得下面方法的參數(shù)進(jìn)行解密以獲取證書,同時(shí)這里需要用到APIv3密鑰,通用的解密方式為:
/**
* 解密響應(yīng)體.
*
* @param apiV3Key API V3 KEY API v3密鑰 商戶平臺(tái)設(shè)置的32位字符串
* @param associatedData response.body.data[i].encrypt_certificate.associated_data
* @param nonce response.body.data[i].encrypt_certificate.nonce
* @param ciphertext response.body.data[i].encrypt_certificate.ciphertext
* @return the string
* @throws GeneralSecurityException the general security exception
*/
public String decryptResponseBody(String apiV3Key,String associatedData, String nonce, String ciphertext) {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec key = new SecretKeySpec(apiV3Key.getBytes(StandardCharsets.UTF_8), "AES");
GCMParameterSpec spec = new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8));
byte[] bytes;
try {
bytes = cipher.doFinal(Base64Utils.decodeFromString(ciphertext));
} catch (GeneralSecurityException e) {
throw new IllegalArgumentException(e);
}
return new String(bytes, StandardCharsets.UTF_8);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException(e);
}
}
回調(diào)的請(qǐng)求體也是此方法進(jìn)行解密。
3. 動(dòng)態(tài)刷新
然后就能拿到微信平臺(tái)證書公鑰。然后你可以定義個(gè)Map,以證書的序列號(hào)為KEY,以證書為Value來(lái)動(dòng)態(tài)刷新,關(guān)鍵偽代碼:
// 定義全局容器 保存微信平臺(tái)證書公鑰 注意線程安全
private static final Map<String, Certificate> CERTIFICATE_MAP = new ConcurrentHashMap<>();
// 下面是刷新方法 refreshCertificate 的核心代碼
String publicKey = decryptResponseBody(associatedData, nonce, ciphertext);
final CertificateFactory cf = CertificateFactory.getInstance("X509");
ByteArrayInputStream inputStream = new ByteArrayInputStream(publicKey.getBytes(StandardCharsets.UTF_8));
Certificate certificate = null;
try {
certificate = cf.generateCertificate(inputStream);
} catch (CertificateException e) {
e.printStackTrace();
}
String responseSerialNo = objectNode.get("serial_no").asText();
// 清理HashMap
CERTIFICATE_MAP.clear();
// 放入證書
CERTIFICATE_MAP.put(responseSerialNo, certificate);
動(dòng)態(tài)刷新的策略就很好寫了:
// 當(dāng)證書容器為空 或者 響應(yīng)提供的證書序列號(hào)不在容器中時(shí) 就應(yīng)該刷新了
if (CERTIFICATE_MAP.isEmpty() || !CERTIFICATE_MAP.containsKey(wechatpaySerial)) {
refreshCertificate();
}
// 然后調(diào)用
Certificate certificate = CERTIFICATE_MAP.get(wechatpaySerial);
4. 總結(jié)
雖然驗(yàn)簽?zāi)悴蛔隹梢阅玫狡渌涌诘捻憫?yīng)結(jié)果,但是從資金安全的角度來(lái)說(shuō)這是十分必要的。同時(shí)因?yàn)槲⑿牌脚_(tái)證書不收我方控制,采取動(dòng)態(tài)刷新也會(huì)更加方便,不必再擔(dān)心過(guò)期的問(wèn)題。本文我們通過(guò)調(diào)用接口拿到密文并解密獲得證書。下一篇我們將通過(guò)獲得的證書進(jìn)行簽名驗(yàn)證來(lái)確保我們的響應(yīng)是微信服務(wù)器發(fā)過(guò)來(lái)的,請(qǐng)關(guān)注:碼農(nóng)小胖哥 及時(shí)獲得相關(guān)的更新。
關(guān)注公眾號(hào):碼農(nóng)小胖哥,獲取更多資訊