1. 背景介紹
v3版微信支付通過商戶證書和平台證書加強了安全性,java版sdk包wechatpay-apache-httpclient內部封裝了安全性相關的簽名、驗簽、加密和解密工作,降低了開發難度。下面幾個特性的實現,更方便了開發者。
- 平台證書自動更新,無需開發者關注平台證書有效性,無需手動下載更新;
- 執行請求時將自動攜帶身份認證信息,並檢查應答的微信支付簽名。
如果文檔中有錯誤的地方,需要路過的大佬指出,我會盡快更改。跪謝。。。
2. API證書
2.1 API 密鑰設置( 一定要設置的!!!! )
請登錄商戶平台進入【賬戶中心】->【賬戶設置】->【API安全】->【APIv3密鑰】中設置 API 密鑰。
具體操作步驟請參見:什么是APIv3密鑰?如何設置?
2.2 獲取 API 證書
請登錄商戶平台進入【賬戶中心】->【賬戶設置】->【API安全】根據提示指引下載證書。
具體操作步驟請參見:什么是API證書?如何獲取API證書?
簡單敘述:(詳細信息請看官方文檔)
證書 | 描述 | 備注 |
---|---|---|
apiclient_cert.p12 | 包含了私鑰信息的證書文件 | 是商戶證書文件(雙向證書) |
apiclient_cert.pem | 從apiclient_cert.p12中導出證書部分的文件,為pem格式 | 簡單理解:公鑰 |
apiclient_key.pem | 從apiclient_key.pem中導出密鑰部分的文件 | 簡單理解:私鑰 |
3. 創建client(請參照官方文檔)
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
@Slf4j
@Aspect
@Component
public class WxPayHttpClientFactory {
public static CloseableHttpClient httpClient;
public static Verifier verifier;
@Before("execution(public * com.xxx.admin.web.controller.xxx.*Controller.*(..))" +
"||execution(public * com.xxx.admin.web.controller.xxx.*Controller.*(..))")
public void initWXPayClient() {
try {
// 加載商戶私鑰(privateKey:私鑰字符串)
PrivateKey merchantPrivateKey = PemUtil
.loadPrivateKey(new ClassPathResource("apiclient_key.pem classpath路徑").getInputStream());
X509Certificate certificate = PemUtil.loadCertificate(new ClassPathResource("apiclient_cert.pem classpath路徑").getInputStream());
String serialNo = certificate.getSerialNumber().toString(16).toUpperCase();
//merchantId:商戶號,serialNo:商戶證書序列號
// 獲取證書管理器實例
CertificatesManager certificatesManager = CertificatesManager.getInstance();
// 向證書管理器增加需要自動更新平台證書的商戶信息
certificatesManager.putMerchant("商戶號", new WechatPay2Credentials("商戶號",
new PrivateKeySigner(serialNo, merchantPrivateKey)), wechatAppPayConfig.api_v3.getBytes(StandardCharsets.UTF_8));
// 從證書管理器中獲取verifier
//版本>=0.4.0可使用 CertificatesManager.getVerifier(mchId) 得到的驗簽器替代默認的驗簽器。
// 它會定時下載和更新商戶對應的微信支付平台證書 (默認下載間隔為UPDATE_INTERVAL_MINUTE)。
verifier = certificatesManager.getVerifier("商戶號");
//創建一個httpClient
httpClient = WechatPayHttpClientBuilder.create()
.withMerchant("商戶號", serialNo, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier)).build();
} catch (IOException e) {
e.printStackTrace();
log.error("加載秘鑰文件失敗");
} catch (GeneralSecurityException e) {
e.printStackTrace();
log.error("獲取平台證書失敗");
} catch (Exception e) {
e.printStackTrace();
}
}
@After("execution(public * com.xxx.admin.web.controller.xxx.*Controller.*(..))" +
"||execution(public * com.xxx.admin.web.controller.xxx.*Controller.*(..))")
public void closeWXClient() {
if (httpClient != null) {
try {
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
官方文檔指出 自動攜帶身份認證信息,並檢查應答的微信支付簽名 。所以上面的代碼就沒攜帶任何簽名。
4. 生成APP支付訂單(請參照官方文檔)
/**
* 微信支付POST請求
*
* @param reqUrl 請求地址 示例:https://api.mch.weixin.qq.com/v3/pay/transactions/app
* @param paramJsonStr 請求體 json字符串 此參數與微信官方文檔一致
* @return 訂單支付的參數
* @throws Exception
*/
public static String V3PayPost(String reqUrl, String paramJsonStr) throws Exception {
//創建post方式請求對象
HttpPost httpPost = new HttpPost(reqUrl);
//裝填參數
StringEntity s = new StringEntity(paramJsonStr, "utf-8");
s.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE,
"application/json"));
//設置參數到請求對象中
httpPost.setEntity(s);
//指定報文頭【Content-type】、【User-Agent】
httpPost.setHeader("Content-type", "application/json");
httpPost.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36");
httpPost.setHeader("Accept", "application/json");
//執行請求操作,並拿到結果(同步阻塞)
CloseableHttpResponse response = WxPayHttpClientFactory.httpClient.execute(httpPost);
int statusCode = response.getStatusLine().getStatusCode();
//獲取數據,並釋放資源
String body = closeHttpResponse(response);
if (statusCode == 200) { //處理成功
switch (reqUrl) {
case "https://api.mch.weixin.qq.com/v3/pay/transactions/app"://返回APP支付所需的參數
return JSONObject.parseObject(body).getString("prepay_id");
case "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds"://返回APP退款結果
return body;
}
}
return null;
}
/**
* 獲取數據,並釋放資源
*
* @param response
* @return
* @throws IOException
*/
public static String closeHttpResponse(CloseableHttpResponse response) throws IOException {
String body = "";
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) { //處理成功
//獲取結果實體
HttpEntity entity = response.getEntity();
if (entity != null) {
//按指定編碼轉換結果實體為String類型
body = EntityUtils.toString(entity, "utf-8");
}
//EntityUtils.consume將會釋放所有由httpEntity所持有的資源
EntityUtils.consume(entity);
}
//釋放鏈接
response.close();
return body;
}
至此就成功 在微信支付服務后台生成預支付交易單,並且拿到prepay_id
,但是調起APP支付,還需其他字段,也就是所謂的二簽。可以直接使用下面的方法WxAppPayTuneUp()
5.APP調起支付(請參照官方文檔)
/**
* 微信調起支付參數
* 返回參數如有不理解 請訪問微信官方文檔
* https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_2_4.shtml
*
* @param prepayId 微信下單返回的prepay_id
* @param appId 應用ID
* @param mch_id 商戶號
* @param private_key_path 私鑰路徑
* @return 當前調起支付所需的參數
* @throws Exception
*/
public static String WxAppPayTuneUp(String prepayId, String appId, String mch_id, String private_key_path) throws Exception {
if (StringUtils.isNotBlank(prepayId)) {
long timestamp = System.currentTimeMillis() / 1000;
String nonceStr = generateNonceStr();
//加載簽名
String packageSign = sign(buildMessage(appId, timestamp, nonceStr, prepayId).getBytes(), private_key_path);
JSONObject jsonObject = new JSONObject();
jsonObject.put("appId", appId);
jsonObject.put("prepayId", prepayId);
jsonObject.put("timeStamp", timestamp);
jsonObject.put("nonceStr", nonceStr);
jsonObject.put("package", "Sign=WXPay");
jsonObject.put("signType", "RSA");
jsonObject.put("sign", packageSign);
jsonObject.put("partnerId", mch_id);
return jsonObject.toJSONString();
}
return "";
}
public static String sign(byte[] message, String private_key_path) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException {
//簽名方式
Signature sign = Signature.getInstance("SHA256withRSA");
//私鑰
sign.initSign(PemUtil
.loadPrivateKey(new ClassPathResource(private_key_path).getInputStream()));
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
}
/**
* 按照前端簽名文檔規范進行排序,\n是換行
*
* @param appId appId
* @param timestamp 時間
* @param nonceStr 隨機字符串
* @param prepay_id prepay_id
* @return
*/
public static String buildMessage(String appId, long timestamp, String nonceStr, String prepay_id) {
return appId + "\n"
+ timestamp + "\n"
+ nonceStr + "\n"
+ prepay_id + "\n";
}
protected static final SecureRandom RANDOM = new SecureRandom();
//生成隨機字符串 微信底層的方法,直接copy出來了
protected static String generateNonceStr() {
char[] nonceChars = new char[32];
for (int index = 0; index < nonceChars.length; ++index) {
nonceChars[index] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".charAt(RANDOM.nextInt("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".length()));
}
return new String(nonceChars);
}
將WxAppPayTuneUp()返回的字符串,響應給前端即可
6.支付通知(異步通知,請參照官方文檔)
@PostMapping("/wxAppPayNotify")
public JSONObject wxAppPayNotify(HttpServletRequest request, HttpServletResponse response) throws IOException, GeneralSecurityException {
//從請求頭獲取驗簽字段
String signature = request.getHeader("Wechatpay-Signature");
String serial = request.getHeader("Wechatpay-Serial");
ServletInputStream inputStream = request.getInputStream();
StringBuilder sb = new StringBuilder();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String s;
//讀取回調請求體
while ((s = bufferedReader.readLine()) != null) {
sb.append(s);
}
String s1 = sb.toString();
//按照文檔要求拼接驗簽串
String verifySignature = request.getHeader("Wechatpay-Timestamp") + "\n"
+ request.getHeader("Wechatpay-Nonce") + "\n" + s1 + "\n";
//使用官方驗簽工具進行驗簽
boolean verify1 = WxPayHttpClientFactory.verifier.verify(serial, verifySignature.getBytes(), signature);
//判斷驗簽的結果
if (!verify1) {
//驗簽失敗,應答接口
//設置狀態碼
response.setStatus(500);
JSONObject jsonResponse = new JSONObject();
jsonResponse.put("code", "FAIL");
jsonResponse.put("message", "失敗");
return jsonResponse;
}
JSONObject parseObject = JSONObject.parseObject(s1);
if ("TRANSACTION.SUCCESS".equals(parseObject.getString("event_type"))
&& "encrypt-resource".equals(parseObject.getString("resource_type"))) {
//通知的類型,支付成功通知的類型為TRANSACTION.SUCCESS
//通知的資源數據類型,支付成功通知為encrypt-resource
JSONObject resourceJson = JSONObject.parseObject(parseObject.getString("resource"));
String associated_data = resourceJson.getString("associated_data");
String nonce = resourceJson.getString("nonce");
String ciphertext = resourceJson.getString("ciphertext");
//解密,如果這里報錯,就一定是APIv3密鑰錯誤
AesUtil aesUtil = new AesUtil(api_v3.getBytes());
String aes = aesUtil.decryptToString(associated_data.getBytes(), nonce.getBytes(), ciphertext);
System.out.println("解密后=" + aes);
//dosomething 處理業務
}
}
JSONObject jsonResponse = new JSONObject();
jsonResponse.put("code", "SUCCESS");
jsonResponse.put("message", "成功");
//設置狀態碼
response.setStatus(200);
return jsonResponse;
}
以上是一套APP下單的流程【APP下單】->【APP調起支付】->【微信支付異步通知】