目录
4.微信支付成功后通知及使用公钥验签
使用API v3微信支付遇到的问题:
1.jdk版本问题, 自动更新证书加载失败, 改成jdk11解决;
2.请求头必须设置请求头 Accept, content-Type
3.通知接收 RequestBody 必须使用Object, 如果用实体类JSON化后属性位置可能发生变化, 影响验签, 不同的JSON类会影响转成字符串后的属性顺序, 我用的是 com.fasterxml.jackson.databind.ObjectMapper 用阿里的JSON类验签不行
4.支付通知本地调试: 配置nginx路由到本地机器, 就可以让微信支付宝直接通知到自己本地服务了
1.微信请求客户端配置
@Configuration
@Data
public class WxpayClientConfig extends WxpayConfig {
private PrivateKey merchantPrivateKey;
private Verifier verifier;
@Bean(name = "wx_client")
public HttpClient getClient() throws Exception {
String merchantSerialNumber = super.getMchSerialNo();
// 加载商户私钥(privateKey:私钥字符串)
this.merchantPrivateKey = this.getPrivateKey(super.getPrivateKey());
// 加载平台证书(mchId:商户号,mchSerialNo:商户证书序列号,apiV3Key:V3秘钥)
this.verifier = new AutoUpdateCertificatesVerifier(
new WechatPay2Credentials(super.getMerchantId(), new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)),
super.getApiV3Key().getBytes("utf-8"));
// 初始化httpClient
Validator validator = new WechatPay2Validator(verifier);
HttpClient httpClient = WechatPayHttpClientBuilder.create()
.withMerchant(super.getMerchantId(), merchantSerialNumber, merchantPrivateKey)
.withValidator(validator).build();
return httpClient;
}
/**
* 获取私钥。
*
* @param filename 私钥文件路径 (required)
* @return 私钥对象
*/
private PrivateKey getPrivateKey(String filename) throws IOException {
String content = new String(Files.readAllBytes(Paths.get(filename)), "utf-8");
try {
String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(
new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持RSA", e);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("无效的密钥格式");
}
}
}
2.生成预付款订单
下面代码中的httpClient为第1步里面注入的@Bean, 这一步请求会返回预付款ID: {
"prepay_id":"xxxxxxxxxxxxxxxxx"
}
String outTradeNo = OrderUtil.getOrderId();
final WxNativeBean wxNativeBean = new WxNativeBean();
wxNativeBean.setAppid(super.getAppId());
wxNativeBean.setMchid(super.getMerchantId());
wxNativeBean.setNotify_url(super.getNotifyUrl());
wxNativeBean.setDescription("充值"); //TODO
wxNativeBean.setOut_trade_no(outTradeNo);
WxNativeBean.Amount amount = new WxNativeBean.Amount();
amount.setTotal((int) (Double.parseDouble(productModel.getTotalAmount()) * 100));
wxNativeBean.setAmount(amount);
final HttpPost httpPost = new HttpPost(super.getNativeUrl());
httpPost.setHeader("Accept", "application/json");
httpPost.setHeader("Content-Type", "application/json;charset=utf-8");
String userAgent = String.format(
"WechatPay-Apache-HttpClient/%s (%s) Java/%s",
getClass().getPackage().getImplementationVersion(),
System.getProperty("os.name") + "/" + System.getProperty("os.version"),
System.getProperty("java.version"));
httpPost.setHeader("User-Agent", userAgent);
log.info("调用微信预支付请求参数: {}", wxNativeBean);
httpPost.setEntity(new StringEntity(objectMapper.writeValueAsString(wxNativeBean), "UTF-8"));
final HttpResponse response = httpClient.execute(httpPost);
int code = response.getStatusLine().getStatusCode();
log.info("微信native请求返回码: {}", code);
final HttpEntity entity = response.getEntity();
String t = EntityUtils.toString(entity);
log.info("微信native请求返回内容: {}", t);
3.拼接字符串使用API v3签名
按照需求拼接字符串, 直接copy微信的描述, 拼接好字符串后使用api v3秘钥进行签名, 第1步初始化是向外暴露了两个变量 : 一个是apiV3私钥对象, 一个是微信公钥证书, 这两个对象非常重要, 私钥对象用来签名, 公钥证书用来验签
private WxOrderInfo getOrderInfo(String prepayId) throws Exception {
final WxOrderInfo wxOrderInfo = new WxOrderInfo();
wxOrderInfo.setAppid(super.getAppId());
wxOrderInfo.setPartnerid(super.getMerchantId());
wxOrderInfo.setPrepayid(prepayId);
wxOrderInfo.setTimestamp(System.currentTimeMillis() + "");
wxOrderInfo.setNoncestr(OrderUtil.getOrderId());
String content = wxpayClientConfig.getAppId() + "\n"
+ System.currentTimeMillis() / 1000 + "\n"
+ OrderUtil.getOrderId() + "\n"
+ prepayId + "\n";
log.info("待签名字符串: {}", content);
wxOrderInfo.setSign(SignUtil.sign(content, wxpayClientConfig.getMerchantPrivateKey()));
return wxOrderInfo;
}
public class SignUtil {
private final static String algorithm = "SHA256withRSA";
public static String sign(String content, PrivateKey privateKey) throws Exception {
Assert.notNull(content, "签名内容不能为空");
final Signature signature = Signature.getInstance(algorithm);
signature.initSign(privateKey);
signature.update(content.getBytes());
return Base64.getEncoder().encodeToString(signature.sign());
}
}
签名完成后把订单发给app调起微信支付
4.微信支付成功后通知
注意, 通知地址必须是https的路径, 接收通知的参数最好用Object对象, 验签后再转为实体类取出参数入库, 成功后必须返回xml的字符串, 否则微信会一直通知
@PostMapping("/wx/notice.html")
public String wxpayCallback(HttpServletRequest request, HttpServletResponse response, @RequestBody Object wxNoticeModel) throws Exception {
log.info("收到微信异步通知 =====> : {}", objectMapper.writeValueAsString(wxNoticeModel));
String timestamp = request.getHeader("Wechatpay-Timestamp");
String nonce = request.getHeader("Wechatpay-Nonce");
String sign = request.getHeader("Wechatpay-Signature");
String serialNumber = request.getHeader("Wechatpay-Serial");
wxpayService.verifyPay(timestamp, nonce, sign, serialNumber, wxNoticeModel);
response.addHeader("Content-type", "text/xml");
return "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
}
验签方法
public void verifyPay(String timestamp, String nonce, String sign, String serialNumber, Object wxNoticeModel) throws Exception {
String content = timestamp + "\n"
+ nonce + "\n"
+ objectMapper.writeValueAsString(wxNoticeModel) + "\n";
if (!wxpayClientConfig.getVerifier().verify(serialNumber, content.getBytes(), sign)) {
throw new Exception("微信支付校验不通过");
}
final WxNoticeModel.NoticeData resource = objectMapper.convertValue(wxNoticeModel, WxNoticeModel.class).getResource();
String str = "";
try {
str = SignUtil.decryptToString(resource.getAssociated_data().getBytes(), resource.getNonce().getBytes(),
resource.getCiphertext(), super.getApiV3Key());
} catch (Exception e) {
log.error("解密微信通知参数错误", e);
throw new Exception("解密微信通知参数错误");
}
Map<String, Object> map = objectMapper.readValue(str, Map.class);
String outTradeNo = map.get("out_trade_no") + "";
String status = map.get("trade_state") + "";
log.info("通知订单号: {}, 订单状态: {}", outTradeNo, status);
}
解密微信参数
public static String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext, String key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
GCMParameterSpec spec = new GCMParameterSpec(128, nonce);
cipher.init(Cipher.DECRYPT_MODE, keySpec, spec);
cipher.updateAAD(associatedData);
return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), StandardCharsets.UTF_8);
}