一、前言
微信小程序应用接入微信支付,java后端实现。
接入前需要准备相关的参数小程序id(即appId),商户号(mchId),apiV3Key,微信针对此商户颁发的公私钥(apiclient_key.pem,apiclient_cert.pem),商户序列号merchantSerialNumber
二、签名和验签
微信支付的相关接口都需要签名。
这里简单解释一下签名和验签。
详细可以参考juejin.cn/post/684490...
1、签名
就如同我们写信的时候签名一样,证明这封信是我本人写的所以需要签名,所以一般是使用私钥。签名的流程一般是对请求体先进行摘要,这个一般是不可逆,通过散列函数等方式,然后拿私钥通过某种算法加密一下就得到了签名值。
2、验签
我们一般是将签名值和请求体数据一块发给服务方,服务方收到请求之后,先对请求体数据进行摘要(散列函数)得数据A,然后对签名值使用公钥解密得到数据B,然后对比数据A和数据B是否一致,如此来保证请求的数据没有被篡改。
三、举例
我们可以使用微信官方的一段例子。pay.weixin.qq.com/docs/mercha...
微信官方的一个接口api.mch.weixin.qq.com/v3/certific...
先不用管这个接口是干嘛用的,我们正常把接口放到postman请求,很明显是请求失败的。
然后我们按照官方给出的格式,对数据进行签名,得到签名值之后按照他们官方得格式,拼接得到一个认证值,再去请求接口就能正常请求得通了。
获取签名值得相关代码如下
java
package com.example;
import com.wechat.pay.java.core.util.PemUtil;
import okhttp3.HttpUrl;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.util.Base64;
public class SignTest {
private String url = "https://api.mch.weixin.qq.com/v3/certificates";
// 官方默认得前缀
private String schema = "WECHATPAY2-SHA256-RSA2048";
private HttpUrl httpurl = HttpUrl.parse(url);
@Test
public void getToken( ) throws Exception {
String nonceStr = "aassccgvd";
long timestamp = System.currentTimeMillis() / 1000;
String message = buildMessage("GET", httpurl, timestamp, nonceStr, "");
// 得到签名值
String signature = sign(message.getBytes(StandardCharsets.UTF_8));
// 认证值需要拼上自己得商户号和商户序列号
String result = schema+" mchid="" + "xxxxx" + "","
+ "nonce_str="" + nonceStr + "","
+ "timestamp="" + timestamp + "","
+ "serial_no="" + "xxxxxx" + "","
+ "signature="" + signature + """;
System.out.println(result);
}
public String sign(byte[] message) throws Exception {
Signature sign = Signature.getInstance("SHA256withRSA");
// 使用私钥进行加密
sign.initSign( PemUtil.loadPrivateKeyFromPath("./apiclient_key.pem"));
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
}
public String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
String canonicalUrl = url.encodedPath();
if (url.encodedQuery() != null) {
canonicalUrl += "?" + url.encodedQuery();
}
return method + "\n"
+ canonicalUrl + "\n"
+ timestamp + "\n"
+ nonceStr + "\n"
+ body + "\n";
}
}
上述代码中PemUtil.loadPrivateKeyFromPath()使用了微信官方SDK中的方法,这里就是根据配置的路径读取文件流,获取文件里面的字符串,然后进行截取,获得私钥串,最后返回PrivateKey对象,这里截图看一下源码,这里大家可以自己写。
四、微信支付
现在我们切回正题,讲一下微信支付的业务流程,我先贴一张微信方法图片。
由图片可以看出,消费者在你的小程序下单某一件商品之后,你自己的商户系统会生成一个订单,然后你的商户系统去调用微信官方的下单接口(这里的下单意思就是需要发起微信支付了,在微信那边下一个单,注意与商户自己的订单的区别),微信官方的接口会给你返一个预支付id(prepayId),然后你把这个预支付id丢给前端,前端会去调微信的组件,拉起支付,组件底层应该也是调用了微信的相关接口,然后完成支付。微信支付完成之后,会调用你系统的回调接口,系统接口需要时https,(本地调式的时候可以开一个https内网穿透),回调之后,商户系统就可以将订单置为已支付。
由上述我们知道,微信支付的接口需要签名和验签,相关接口属实复杂,所以我们可以直接使用微信官方提供的相关SDK,该SDK帮我们省去了签名和验签的繁琐步骤。
xml
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.12</version>
</dependency>
大家可以去github查找最近的版本。
使用方式也很简单,先往Ioc容器放一个配置对象
kotlin
package com.example.wechat.official.config;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class WxPayConfig {
@Value("${wx.pay.appId}")
private String appId;
@Value("${wx.pay.mchId}")
private String mchId;
@Value("${wx.pay.privateKeyPath}")
private String privateKeyPath;
@Value("${wx.pay.merchantSerialNumber}")
private String merchantSerialNumber;
@Value("${wx.pay.apiV3Key}")
private String apiV3Key;
@Bean
public Config setWxPayConfig() {
Config config =
new RSAAutoCertificateConfig.Builder()
.merchantId(mchId)
.privateKeyFromPath(privateKeyPath)
.merchantSerialNumber(merchantSerialNumber)
.apiV3Key(apiV3Key)
.build();
return config;
}
}
然后就可以按照官方的接口文档塞参数请求接口了,(官方文档pay.weixin.qq.com/docs/mercha...) 注意我这里使用的是SDK中jsapi包下面的service和model,微信官方提供了好几种不同的支付,而不同的支付使用的是不同包下面的service和model
附上相关代码
scss
@RestController
@RequestMapping("/test/pay")
@Slf4j
public class OfficialAppletPayController {
@Value("${wx.pay.appId}")
private String appId;
@Value("${wx.pay.mchId}")
private String mchId;
@Autowired
private Config config;
@PostMapping("/createOrder")
public Object createOrder() {
// 构建service
JsapiService service = new JsapiService.Builder().config(config).build();
// request.setXxx(val)设置所需参数,具体参数可见Request定义
PrepayRequest request = new PrepayRequest();
request.setAppid(appId);
request.setMchid(mchId);
request.setDescription("测试商品标题");
// 回调接口url
request.setNotifyUrl("https://notify_url");
// 商户系统自己的订单号,这里先随机生成,大家可以根据自己的系统生成规则生成订单号,
// 要保证在自己的系统中唯一就行。
String orderNo = RandomStringUtils.random(20, true, true).toUpperCase();
request.setOutTradeNo(orderNo);
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, 15);
Date time = calendar.getTime();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
String expiredTime = sdf.format(time);
request.setTimeExpire(expiredTime);
// 设置openid 相当于该微信用户的一个标识
Payer payer = new Payer();
payer.setOpenid("xxxxxxxxxxxxxxx");
request.setPayer(payer);
// 请求付钱的金额,以分为单位
Amount amount = new Amount();
amount.setTotal(1);
amount.setCurrency("CNY");
request.setAmount(amount);
// 调用下单方法,得到应答
PrepayResponse response = service.prepay(request);
System.out.println(response);
return response;
}
}
这里我们按照官方的接口文档,把必填项值塞一下,就可以正常请求得到prepayId,当然,请求的时候还是需要签名的,只不是这些工作被SDK帮忙做了。我们可以看一下SDK底层的相关源码。
这里getSchema()其实就是返回上述的默认值"WECHATPAY2-SHA256-RSA2048"
五、拉起支付
后端生成prepayId之后,并不能直接把这个prepayId给前端去调用微信的组件,因为微信支付组件底层还是要调微信的接口还是要签名的,所以一般是后端再对prepayId做一次签名,然后将该请求体返回前端,前端拉起支付,完成付款。
六、微信回调
消费者再付完钱之后,就会回调商户系统的接口(下单时候配置的notifyUrl,一般做成配置项),我们这里简称回调接口,商户系统可以在这个接口里面对商户的订单置为已完成态。
代码如下
java
package com.example.wechat.official.controller;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.exception.ValidationException;
import com.wechat.pay.java.core.notification.NotificationConfig;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.service.partnerpayments.jsapi.model.Transaction;
import org.apache.commons.lang3.RandomStringUtils;
import com.wechat.pay.java.service.payments.jsapi.JsapiService;
import com.wechat.pay.java.service.payments.jsapi.model.Amount;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayResponse;
import com.wechat.pay.java.service.payments.jsapi.model.Payer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* 微信官方指定SDK--小程序支付
*/
@RestController
@RequestMapping("/test/pay")
@Slf4j
public class OfficialAppletPayController {
@Value("${wx.pay.appId}")
private String appId;
@Value("${wx.pay.mchId}")
private String mchId;
@Autowired
private Config config;
@PostMapping("/notify")
public Map wechatNotify(HttpServletRequest request) {
String requestBody = getRequestBody(request).trim();
log.info("支付结果异步接口,请求参数[转换前]:{}", requestBody);
requestBody = notificationAttrRearrangement(requestBody);
log.info("支付结果异步接口,请求参数[转换后]:{}", requestBody);
// 构造 RequestParam
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(request.getHeader("Wechatpay-Serial"))
.nonce(request.getHeader("Wechatpay-Nonce"))
.signature(request.getHeader("Wechatpay-Signature"))
.timestamp(request.getHeader("Wechatpay-Timestamp"))
.body(requestBody)
.build();
// 如果已经初始化了 RSAAutoCertificateConfig,可直接使用
// 初始化 NotificationParser
NotificationParser parser = new NotificationParser((NotificationConfig)config);
Transaction transaction;
try {
// 以支付通知回调为例,验签、解密并转换成 Transaction
transaction = parser.parse(requestParam, Transaction.class);
} catch (ValidationException e) {
// 签名验证失败,返回 401 UNAUTHORIZED 状态码
log.error("sign verification failed", e);
return failResponse();
}
if (Objects.nonNull(transaction)) {
try {
// 商户自己的业务逻辑(将商户自己的订单号置为已支付状态等等)
} catch (Exception e) {
log.error("complete intentionOrder failed", e);
return failResponse();
}
// 处理成功,返回 200 OK 状态码
return sucessResponse();
}
// 如果处理失败,应返回 4xx/5xx 的状态码,例如 500 INTERNAL_SERVER_ERROR
return failResponse();
}
private Map sucessResponse() {
HashMap<String, String> resultMap = new HashMap<>();
resultMap.put("code","SUCCESS");
return resultMap;
}
private Map failResponse() {
HashMap<String, String> resultMap = new HashMap<>();
resultMap.put("code","FAIL");
return resultMap;
}
/**
* 获取请求头数据
*/
private String getRequestBody(HttpServletRequest request) {
StringBuffer sb = new StringBuffer();
try (ServletInputStream inputStream = request.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
) {
String line;
while ((line = reader.readLine()) != null) sb.append(line);
} catch (IOException e) {
log.error("读取数据流异常:{}", e);
}
return sb.toString();
}
/**
* 对字段重排序用以符合签名校验
* @param requestBody /
* @return /
*/
private String notificationAttrRearrangement(String requestBody) {
JSONObject jsonObject = JSON.parseObject(requestBody);
final String id = jsonObject.getString("id");
final String createTime = jsonObject.getString("create_time");
final String eventType = jsonObject.getString("event_type");
final String resourceType = jsonObject.getString("resource_type");
final String summary = jsonObject.getString("summary");
JSONObject resource = jsonObject.getJSONObject("resource");
String algorithm = resource.getString("algorithm");
String ciphertext = resource.getString("ciphertext");
String associatedData = resource.getString("associated_data");
String originalType = resource.getString("original_type");
String nonce = resource.getString("nonce");
return "{" +
""id":"" + id + ""," +
""create_time":"" + createTime + ""," +
""resource_type":"" + resourceType + ""," +
""event_type":"" + eventType + ""," +
""summary":"" + summary + ""," +
""resource":{" +
""original_type":"" + originalType + ""," +
""algorithm":"" + algorithm + ""," +
""ciphertext":"" + ciphertext + ""," +
""associated_data":"" + associatedData + ""," +
""nonce":"" + nonce + """ +
"}" +
"}";
}
}
这个时候我们属于是被调用方了,所以这个时候按照微信官方的建议,我们需要对这个回调使用公钥验签了,当时验签的过程SDK也帮我们完成了,验完签之后,还要对数据解密,得到Transaction对象,该对象的结构可以参考微信官方文档(pay.weixin.qq.com/docs/mercha...%25E3%2580%2582 "https://pay.weixin.qq.com/docs/merchant/apis/mini-program-payment/payment-notice.html)%E3%80%82")
这里我们一起走读一下验签,解密的相关源码。 入口是是方法parser.parse(requestParam, Transaction.class)
再看一下解析报文的相关代码
七、最后
此外,市面上面还有一些个人写其他SDK,也是比较好用的,比较经典的比如
xml
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>4.5.0</version>
</dependency>
这个SDK下单接口里面会将返回的prepayId做一下签名,就不需要我们自己手动再去签名了。这里也奉赠源码截图。
其中签名的数据体的格式得按照微信官方得来
至此,微信支付的所有流程都结束了,当然这里还可能涉及到商户系统去查询微信官方的订单情况,取消微信官方的订单等等,相关接口比较简单,SDK中对应的也都封装好了,大家有兴趣可以自行查看。然后有什么问题,不正之处或者建议,大家可以留言讨论。
原创不易,转载请贴出处。