前言
正好最近项目中有需要做微信支付,跟着官方文档写下来坑还是踩了不少,于是写了这篇流程给自己长长记性,代码比较粗糙大家图一乐就好。
官方文档
官方接入指引--微信支付开发者文档
所用依赖
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.2.3</version>
</dependency>
//用来生成随机字符串的工具类
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.15</version>
</dependency>
常量解释
以下常量需要通过注册商户号获得
- APPID: 小程序ID,在微信开发平台里可以获得,格式为wx************
- API_V3_KEY:在微信商户中心的账号中心里获得,用来解密微信回调的报文
- MCH_SERIAL_NO:商户证书序列号,在微信商户中心的账号中心里获得。
- MCH_ID:商户号,开通商户后那一串数字就是商户号了。
- PRIVATE_KEY: 商户秘钥,商户在注册证书后从apiClient_key.pem文件中可以获得,商户秘钥是用来给交易签名的重要数据,请不要暴露给外部。
以下常量需要后端自行设置
- PACKAGE:签名固定字符串,小程序支付固定值为"RSA"
- NOTIFY_URL:回调地址,支付成功后微信回调报文的地址,请设置为外网可见。
流程说明
创建订单
平台用户购买平台商品,前端将动态生成的商品详情,商品价格以及订单号传入创单接口,后端接收参数,请求微信支付系统接口,获取接口返回的预支付ID。 以下是后端代码 注意,代码中WeXinPayConstant里存放的是常量值,rootNode中应填写自己项目对应的值
//创建HttpClient
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
new ByteArrayInputStream(WeXinPayConstant.PRIVATE_KEY.getBytes("utf-8")));
//使用自动更新的签名验证器,不需要传入证书
AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
new WechatPay2Credentials(WeXinPayConstant.MCH_ID, new PrivateKeySigner(WeXinPayConstant.MCH_SERIAL_NO, merchantPrivateKey)),
WeXinPayConstant.API_V3_KEY.getBytes(StandardCharsets.UTF_8));
CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create()
.withMerchant(WeXinPayConstant.MCH_ID, WeXinPayConstant.MCH_SERIAL_NO, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier))
.build();
//------------------------创建订单----------------------------
//请求URL
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type","application/json; charset=utf-8");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectMapper objectMapper = new ObjectMapper();
ObjectNode rootNode = objectMapper.createObjectNode();
rootNode.put("mchid",WeXinPayConstant.MCH_ID)//商户号
.put("appid", WeXinPayConstant.APPID)// 公众号id
.put("description", desc)//商品描述
.put("notify_url", WeXinPayConstant.NOTIFY_URL)//支付回调地址
.put("out_trade_no", orderNumber);//自己平台订单号
rootNode.putObject("amount")
.put("total", amount);//价格 单位:分
rootNode.putObject("payer")
.put("openid", openId);//支付用户的OpenId
objectMapper.writeValue(bos, rootNode);
httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
CloseableHttpResponse response = httpClient.execute(httpPost);
//-----------------获得预支付ID------------------
String bodyAsString = EntityUtils.toString(response.getEntity());
数据处理
后台获取预支付id后,应向前端返回数据以供前端调用微信支付系统的接口,在接口文档中我们可以看到,前端需要调用wx.requestPayment(OBJECT)发起微信支付。 其中有5项必填项,分别为:时间戳(timeStamp)、随机字符串(nonceStr)、订单详情扩展字符串(package)、签名方式(paySign)、签名(paySign) 这里要注意 时间戳属性要求的是10位字符串即需要精确到秒,我们在获取时间戳时需要System.currentTimeMillis()/1000L来获取对应数据 以下是后端代码
//-----------------数据加密----------------------
String timestamp=System.currentTimeMillis()/1000L+"";
String nonce= RandomUtil.randomString(32);
StringBuffer buffer=new StringBuffer();
//应用ID
buffer.append(WeXinPayConstant.APPID).append("\n");
//时间戳
buffer.append(timestamp).append("\n");
//随机字符串
buffer.append(nonce).append("\n");
//通过JsonNode读取返回的json对象
JsonNode node=objectMapper.readTree(bodyAsString);
//预支付会话ID
buffer.append("prepay_id=");
buffer.append(node.get("prepay_id")).append("\n");
//获得加密后的数据
String cipherText = WeXinPayUtil.sign(buffer.toString().getBytes());
//将数据进行包装后返回前端
WeXinPayReturn weXinPayReturn=new WeXinPayReturn();
weXinPayReturn.setNonceStr(nonce);
weXinPayReturn.setSignType(WeXinPayConstant.PACKAGE);
weXinPayReturn.setPackageCode("prepay_id="+node.get("prepay_id").toString());
weXinPayReturn.setPaySign(cipherText);
weXinPayReturn.setTimestamp(timestamp);
return weXinPayReturn;
生成签名的方法
//生成签名
public static String sign(byte[] message) throws Exception{
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(getPrivateKey(WeXinPayConstant.PRIVATE_KEY));
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
}
public static PrivateKey getPrivateKey(String key) throws IOException {
try {
//获取签名内容
String privateKey = key.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("无效的密钥格式");
}
}
发起支付
前端调用微信支付
wx.requestPayment({
timeStamp: '',
nonceStr: '',
package: '',
signType: 'MD5',
paySign: '',
success (res) { },
fail (res) { }
})
至此前半段支付功能完成
回调通知
用户付款操作完成后,微信会通过POST请求给回调地址NOTIFY_URL发送通知相关支付结果。 回调地址即下单接口中的"notify_url"参数,地址必须为https地址且外网可见,不能带任何参数。
通知参数 回调通知会在请求头中附带数据: 1.Wechatpay-Timestamp 时间戳 2.Wechatpay-Nonce 随机字符串 3.Wechatpay-Serial 序列号 4.Wechatpay-Signature 通知签名
通知应答 支付通知必须响应应答码为200或204才会被当做正常接收,当回调处理异常时,应答的HTTP状态码应为500,或者4xx。 代码中我们应将回调应答以Map形式返回
示例格式
{
"code": "SUCCESS",
"message": "成功"
}
以下是后端代码
public Map payCallBack(HttpServletRequest request){
Map result = new HashMap();
result.put("code","FAIL");
try {
StringBuffer signStr=new StringBuffer();
signStr.append(request.getHeader("Wechatpay-Timestamp")).append("\n");
signStr.append(request.getHeader("Wechatpay-Nonce")).append("\n");
BufferedReader br=request.getReader();
String str = null;
//报文
StringBuffer buffer = new StringBuffer();
while ((str=br.readLine())!=null){
buffer.append(str);
}
signStr.append(buffer.toString()).append("\n");
//验签
if (!WeXinPayUtil.signVerify(request.getHeader("Wechatpay-Serial"),
signStr.toString(),
request.getHeader("Wechatpay-Signature"))){
result.put("message","sign error");
return result;
}
//解密报文 获取返回对象
String resource = WeXinPayUtil.decryptOrder(buffer.toString());
ReturnResource returnResource= JSONArray.parseObject(resource,ReturnResource.class);
------操作ReturnResource类进行订单验证业务------
业务代码省略
---------------------------------------------
}catch (IOException e){
e.printStackTrace();
}
return result;
}
ReturnResource类这里提供一个JSON字符串转实体类的网站:
在线JSON字符串转Java实体类(JavaBean、Entity)-BeJSON.com
验签方法
public static boolean signVerify(String serial,String message,String signature){
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
new ByteArrayInputStream(WeXinPayConstant.PRIVATE_KEY.getBytes(StandardCharsets.UTF_8)));
AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
new WechatPay2Credentials(WeXinPayConstant.MCH_ID, new PrivateKeySigner(WeXinPayConstant.MCH_SERIAL_NO, merchantPrivateKey)),
WeXinPayConstant.API_V3_KEY.getBytes(StandardCharsets.UTF_8));
return verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature);
}
解密报文方法
public static String decryptOrder(String body){
try {
AesUtil aesUtil=new AesUtil(WeXinPayConstant.API_V3_KEY.getBytes(StandardCharsets.UTF_8));
ObjectMapper objectMapper = new ObjectMapper();
JsonNode node=objectMapper.readTree(body);
JsonNode resource=node.get("resource");
String cipherText=resource.get("ciphertext").toString().replace("\"","");
String associatedData=resource.get("associated_data").toString().replace("\"","");
String nonce=resource.get("nonce").toString().replace("\"","");
return aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),nonce.getBytes(StandardCharsets.UTF_8),cipherText);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
|