最近在开发一款小程序用到微信支付,中间还是遇到了一些小坑,在这里记录一下,希望可以给到第一次做支付的童鞋一些帮助,此教程以小程序支付为例,其他支付与此类似
我们在做到微信支付时,首先需要理解微信支付的整个流程,官方文档这里已经介绍的很详细了,直接上图:
开发步骤逻辑说明 :(以下每个步骤会在后续中详细讲解)
1、用户在小程序端调用后台自己写的下单接口,在这个接口中生成商户自己的订单。
2、后端在自己写的下单接口中 调用微信的统一下单接口,微信会返回一个预支付订单id。
3、后端在收到微信返回的预支付订单id后,需要将数据进行加签处理,并将数据返回给前端小程序。
4、小程序调起微信支付接口,其中参数见微信官方文档(注意:小程序再在起微信支付接口所需要的参数是后端签名后返回的数据)。
5、用户授权且支付成功后,微信会根据后端在预下单时填写的支付回调地址,异步通知商户后端支付信息。
6、后端在收到微信支付通知时,需要进行验签处理。验签通过后,后端执行自己的后续支付成功逻辑。
微信官方提供了相关的SDK,我们用微信提供的SDK就可以了,不要在看其他的各种自己写的连接教程、加签验签、调用证书等等。直接使用官方提供的SDK省时省力还安全。/font>
官方地址:微信支付API v3的Apache HttpClient扩展,实现了请求签名的生成和应答签名的验证
1、初始化HttpClient,我们直接使用官方推荐的方法:自动更新证书功能(不用考虑证书的问题)
使用 AutoUpdateCertificatesVerifier 类替代默认的验签器。它会在构造时自动下载商户对应的微信支付平台证书,并每隔一段时间(默认为1个小时)更新证书。
代码如下(示例):
具体参数参考官方文档:微信支付统一下单
/**
* 初始化微信支付链接
* privateKey 商户API私钥
* apiv3 API v3密钥
* mchid 商户id
* serialNumber 商户API证书的证书序列号
* @return
*/
public HttpClient initWChant(){
PrivateKey merchantPrivateKey = PemUtil
.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes(StandardCharsets.UTF_8)));
String apiv3 = WChantPay.getApiv3();
String mchid = WChantPay.getMchid();
String serialNumber = WChantPay.getSerialNumber();
AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
new WechatPay2Credentials(mchid, new PrivateKeySigner(serialNumber, merchantPrivateKey)),
apiv3.getBytes(StandardCharsets.UTF_8));
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(WChantPay.getMchid(), serialNumber, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier));
return builder.build();
}
代码如下(示例):
**
* 微信统一下单
* @param User 用户信息
* @param wxpayDetail 后端自己生成的订单信息
* @param httpClient 上一步生成的微信链接对象
* @return
* @throws Exception
*/
public Map<String,String> pay(User user,WxpayDetail wxpayDetail,HttpClient httpClient){
HttpPost httpPost = new HttpPost(WChantPay.getPayUrl());
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type","application/json; charset=utf-8");
org.apache.commons.io.output.ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectMapper objectMapper = new ObjectMapper();
ObjectNode rootNode = objectMapper.createObjectNode();
//商户id
rootNode.put("mchid",WChantPay.getMchid())
//小程序id
.put("appid", WChantPay.getAppId())
//描述
.put("description", "商品描述")
//微信通知回调地址
.put("notify_url", WChantPay.getNotifyUrl())
//商户订单id
.put("out_trade_no", wxpayDetail.getHomeOrderId());
//如果前端直接传的是分此处不需要再转
int round = Math.round(wxpayDetail.getTotal() * 100);
rootNode.putObject("amount")
//支付金额,单位是(分)
.put("total", round);
rootNode.putObject("payer")
//支付用户的openid
.put("openid", merchantUsers.getOpenId());
objectMapper.writeValue(bos, rootNode);
httpPost.setEntity(new StringEntity(bos.toString("UTF-8")));
HttpResponse response = httpClient.execute(httpPost);
String bodyAsString = EntityUtils.toString(response.getEntity());
//微信返回的预支付id
JSONObject jsonObject = JSONObject.parseObject(bodyAsString);
}
/**
* 构建签名
* @param prepay_id 微信预订单返回的预支付订单id
* @param orderId 商户自己的订单号
* @return 将这个数据返回给小程序前端
*/
public Map<String,String> responseSign(String prepay_id,String orderId) {
Map<String,String> map=new HashMap();
map.put("appId",WChantPay.getAppId());
long timeStamp = System.currentTimeMillis()/1000;
map.put("timeStamp",timeStamp+"");
String nonceStr = WXPayUtil.generateNonceStr();
map.put("nonceStr", nonceStr);
map.put("package", "prepay_id="+ prepay_id);
String
//一定要注意,最后也要加个\n
s=WChantPay.getAppId()+"\n"+timeStamp+"\n"+nonceStr+"\n"+"prepay_id="+prepay_id.toString()+"\n";
String paySign = WXPayUtil.sign(s.getBytes(StandardCharsets.UTF_8));
map.put("paySign",paySign);
map.put("signType","RSA");
map.put("orderId",orderId);
return map;
}
到此第一步功能就完成了,接下来就是小程序使用后端返回的数据调用微信的支付接口 具体文档参见:小程序调起支付
微信再收到小程序支付后,会将支付结果异步通知给商户的后端,通知地址为预下单时 notify_url 字段,参见上面的统一下单代码。
接收通知这个接口我们分为两个步骤,第一个是进行验签 第二个是进行数据解密
官方文档:微信支付通知
//微信验签
public boolean notify(HttpServletRequest request){
//获取微信返回的body中的数据
String body= ReadAsChars(request);
//证书序列号
String serial = request.getHeader("Wechatpay-Serial");
//微信应答签名
String signature= request.getHeader("Wechatpay-Signature");
//时间戳
String timesTamp = request.getHeader("Wechatpay-Timestamp");
//随机字符串
String nonce = request.getHeader("Wechatpay-Nonce");
//初始化微信连接 参数与上面初始化微信连接 一致
PrivateKey merchantPrivateKey = PemUtil
.loadPrivateKey(new ByteArrayInputStream(WChantPayService.privateKey.getBytes(StandardCharsets.UTF_8)));
AutoUpdateCertificatesVerifier autoUpdateCertificatesVerifier = new AutoUpdateCertificatesVerifier(
new WechatPay2Credentials(WChantPay.getMchid(), new PrivateKeySigner(WChantPay.getSerialNumber(), merchantPrivateKey)),
WChantPay.getApiv3().getBytes(StandardCharsets.UTF_8));
]//构建验签串
String sign = wChantPayService.responseSign(timesTamp, nonce, body);
System.out.println("签名串:" + sign);
//验签
boolean verify = autoUpdateCertificatesVerifier.verify(serial, sign.getBytes(StandardCharsets.UTF_8), signature);
System.out.println("验签:" + verify);
}
/**
* 获取request中的请求body
*/
public static String ReadAsChars(HttpServletRequest request) {
BufferedReader br = null;
StringBuilder sb = new StringBuilder("");
try {
br = request.getReader();
String str;
while ((str = br.readLine()) != null) {
sb.append(str);
}
br.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != br) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return sb.toString();
}
/**
* 构造验签名串.
*
* @param wechatpayTimestamp HTTP头 Wechatpay-Timestamp 中的应答时间戳。
* @param wechatpayNonce HTTP头 Wechatpay-Nonce 中的应答随机串
* @param body 响应体
* @return the string
*/
public String responseSign(String wechatpayTimestamp, String wechatpayNonce, String body) {
return Stream.of(wechatpayTimestamp, wechatpayNonce, body)
.collect(Collectors.joining("\n", "", "\n"));
}
收到微信的数据后,将数据记得转换成json 或者自己定义的对象,取出你需要的数据,比较简单这里就不再贴代码了
public void decodeData(String associatedData,String nonce,String ciphertext) {
//微信官方提供的解密工具
AesUtil aesUtil=new AesUtil(WChantPay.getApiv3().getBytes(StandardCharsets.UTF_8));
String rsData = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext);
}
package com.home.machine.pay.utils;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
/**
* @author wasin
* @description: date 2021-05-08
*/
public class AesUtil {
static final int KEY_LENGTH_BYTE = 32;
static final int TAG_LENGTH_BIT = 128;
private final byte[] aesKey;
public AesUtil(byte[] key) {
if (key.length != KEY_LENGTH_BYTE) {
throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
}
this.aesKey = key;
}
/**
*
* @param associatedData 微信通知返回的附加数据
* @param nonce 微信通知返回的随机字符串
* @param ciphertext 待解密的数据
* @return
* @throws GeneralSecurityException
* @throws IOException
*/
public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext)
throws GeneralSecurityException, IOException {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD(associatedData);
byte[] decode = Base64.getDecoder().decode(ciphertext);
return new String(cipher.doFinal(decode), "utf-8");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException(e);
}
}
}
WXPayUtil相关代码
public static String sign(byte[] message){
Signature sign = null;
try {
sign = Signature.getInstance("SHA256withRSA");
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new
FileInputStream(WChantPayConfig.getPrivateKeyUrl()));//商户私钥地址 F:/xxx/xxx/apiclient_key.pem
sign.initSign(merchantPrivateKey);
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException | FileNotFoundException e) {
e.printStackTrace();
return null;
}
}
至此微信的支付就完成了,欢迎大家一起交流。