微信最新版本的支付,v3,不用手动签名就可以发起支付,但是前端发起支付的时候需要签名,这时候就需要后端签名给前端使用,这里讲了支付,签名和回调,一站式完成。
支付流程:后端发起支付请求,生成一个预支付会话id,然后把相关数据返回给前端,前端发起支付,支付完之后微信调用回调接口,处理相关业务,结束。
开始上代码:
引入依赖
com.github.wechatpay-apiv3
wechatpay-apache-httpclient
0.4.4
获取证书
private WechatPayHttpClientBuilder getWechatPayHttpClientBuilder() throws IOException, GeneralSecurityException, HttpCodeException, NotFoundException {
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
new FileInputStream(certUrl));
CertificatesManager certificatesManager = CertificatesManager.getInstance();
// 向证书管理器增加需要自动更新平台证书的商户信息
certificatesManager.putMerchant(merchantId, new WechatPay2Credentials(merchantId,
new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)), apiV3Key.getBytes(StandardCharsets.UTF_8));
// ... 若有多个商户号,可继续调用putMerchant添加商户信息
// 从证书管理器中获取verifier
Verifier verifier = certificatesManager.getVerifier(merchantId);
return WechatPayHttpClientBuilder.create()
.withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier));
}
里面的参数说明一下:
certUrl:指的是下载的证书地址(apiclient_key.pem),这个文件的地址,开通微信支付的时候可以下载证书,里面有这个文件
merchantId: 商户id
merchantSerialNumber:商户序列号
apiV3Key:生成证书的时候需要设置一个密码,就是这个密码
下单:
public WxPayResp payOrder(String orderId, Double amount, String userOpenId) {
try {
//获取证书私钥
WechatPayHttpClientBuilder builder = getWechatPayHttpClientBuilder();
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签
CloseableHttpClient httpClient = builder.build();
HttpPost httpPost = new HttpPost(JS_API_URL);
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type", "application/json; charset=utf-8");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectMapper objectMapper = new ObjectMapper();
//支付金额单位是:分
int total = (int) (amount * 100);
ObjectNode rootNode = objectMapper.createObjectNode();
rootNode.put("mchid", merchantId)
.put("appid", appId)
.put("description", "微信支付")
.put("notify_url", notifyUrl)//回调地址
.put("out_trade_no", orderId);
rootNode.putObject("amount")
.put("total", total);
rootNode.putObject("payer")
.put("openid", userOpenId);//支付者的openId
objectMapper.writeValue(bos, rootNode);
httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
CloseableHttpResponse response = httpClient.execute(httpPost);
String bodyAsString = EntityUtils.toString(response.getEntity());
//获取预支付会话id
String prepay_id = JsonUtil.fromJson(bodyAsString, JSONObject.class).getString("prepay_id");
//生成支付所需的参数
String nonceStr = RandomUtil.getRandomToInviteCodeToNum(10);
long timestamp = System.currentTimeMillis() / 1000;
//生成签名,给前端使用,和后端请求微信端没有半点关系,主要给前端使用
String token = getToken(timestamp, nonceStr, "prepay_id="+prepay_id);
//封装前端支付所需的参数
//这里非常注意预支付会话id 返回格式为:"prepay_id="+prepay_id,必须加上前面的prepay_id,否则前端调用支付的时候报没有填入total_fee
WxPayResp resp = WxPayResp.builder()
.appId(appId)
.nonceStr(nonceStr)
.pakeage("prepay_id="+prepay_id)
.timestamp(timestamp)
.signType("RSA")
.paySign(token)
.build();
return resp;
} catch (IOException e) {
e.printStackTrace();
} catch (GeneralSecurityException e) {
e.printStackTrace();
} catch (HttpCodeException e) {
e.printStackTrace();
} catch (NotFoundException e) {
e.printStackTrace();
}
return null;
}
这里的签名千万要注意,这个签名不是用来后端请求微信端的签名,而是返回给前端使用的签名,所以签名规则也是前端的签名规则,接口文档是:
一定要注意这点,这个坑我已经踩过了,希望朋友们别在踩了
剩下的具体签名方式:
private String getToken( long timestamp, String nonceStr, String pakeage) {
String message = buildMessage(timestamp, nonceStr, pakeage);
String signature = null;
try {
signature = sign(message.getBytes("utf-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return signature;
}
private String buildMessage( long timestamp, String nonceStr, String pakeage) {
return appId + "\n"
+ timestamp + "\n"
+ nonceStr + "\n"
+ pakeage + "\n";
}
private String sign(byte[] message) {
Signature sign = null;
try {
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
new FileInputStream(certUrl));
sign = Signature.getInstance("SHA256withRSA");
sign.initSign(merchantPrivateKey);
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (SignatureException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return null;
}
这样就完成了后端向微信发起支付,返回一个预支付会话id,然后前端拿到后端给的数据以及后端的签名,前端就可以发起支付了。剩下的就是回调了
回调逻辑---微信请求 回调接口,返回的数据是加密的,需要自行解密,才可以拿到相关数据
代码如下:
接收消息
/**
* 回调
* @param request
* @param response
*/
public void wxMergePayNotify(HttpServletRequest request, HttpServletResponse response) {
//给微信的回应
Map result = new HashMap<>(2);
//解密数据
String data = this.payRequestDecryption(request);
if (data == null){
result.put("code","FAILED");
result.put("message","失败");
String json = JSON.toJSONString(result);
PrintWriter out = null;
try {
out = response.getWriter();
} catch (IOException e) {
e.printStackTrace();
}
out.write(json);
//return result;
}
log.info("微信支付处理后的数据data={}", data);
JSONObject jsonObject = JSONObject.parseObject(data);
//TODO .......业务逻辑处理
try {
bindingBusinessService.payResult(jsonObject);
} catch (Exception e) {
System.out.println("支付成功,逻辑处理失败---,支付返回的数据---"+jsonObject);
e.printStackTrace();
}
log.info("微信支付回调成功");
result.put("code","SUCCESS");
result.put("message","成功");
String json = JSON.toJSONString(result);
PrintWriter out = null;
try {
out = response.getWriter();
} catch (IOException e) {
e.printStackTrace();
}
out.write(json);
}
签名验证:
/**
* 签名校验
* url https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter5_1_13.shtml
*/
public String verifySign(HttpServletRequest request) throws Exception {
//检查header
String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
for (String headerName : headers) {
if (request.getHeader(headerName) == null) {
log.info("{} is null", headerName);
return null;
}
}
//检查时间
String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp));
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) {
log.info("超过应答时间");
return null;
}
//获取微信返回的参数
String data;
try {
data = request.getReader().lines().collect(Collectors.joining());
} catch (IOException e) {
log.error("获取微信V3回调参数失败",e);
return null;
}
//校验签名
String nonce = request.getHeader(WECHAT_PAY_NONCE);
String message = timestamp + "\n" + nonce + "\n" + data + "\n";
String serial = request.getHeader(WECHAT_PAY_SERIAL);
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
Verifier verifier = getVerifier();
if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
log.info("签名校验失败");
return null;
}
return data;
}
private Verifier getVerifier() throws IOException, GeneralSecurityException, HttpCodeException, NotFoundException {
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
new FileInputStream(certUrl));
CertificatesManager certificatesManager = CertificatesManager.getInstance();
certificatesManager.putMerchant(merchantId,
new WechatPay2Credentials(merchantId,
new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)),
apiV3Key.getBytes(StandardCharsets.UTF_8));
// 从证书管理器中获取verifier
return certificatesManager.getVerifier(merchantId);
}
验证签名,并解密数据:
/**
* v3支付回调数据校验
*/
public String payRequestDecryption(HttpServletRequest request){
//校验签名
String data = null;
try {
data = this.verifySign(request);
} catch (Exception e) {
e.printStackTrace();
}
if (data == null){
return null;
}
JSONObject jsonObject = JSONObject.parseObject(data);
System.out.println(jsonObject);
String eventType = jsonObject.getString("event_type");
String resourceType = jsonObject.getString("resource_type");
if (!Objects.equals(eventType,"TRANSACTION.SUCCESS") || !Objects.equals(resourceType,"encrypt-resource")){
log.info("不是支付通知不处理:{}",data);
return null;
}
//参数解密
JSONObject resource = jsonObject.getJSONObject("resource");
String ciphertext = resource.getString("ciphertext");
String nonce = resource.getString("nonce");
String associatedData = resource.getString("associated_data");
AesUtil aesUtil = new AesUtil(apiV3Key.getBytes(StandardCharsets.UTF_8));
String result = null;
try {
result = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),nonce.getBytes(StandardCharsets.UTF_8),ciphertext);
} catch (GeneralSecurityException e) {
log.error("微信v3解密异常",e);
}
System.out.println("解密之后的数据是:"+result);
return result;
}
至此,结束。