一、业务介绍
1.支付流程
这是微信官方的流程图,简单介绍就是,用户点击支付->调用后台预支付接口->返回结果给前端->前端拉起支付->用户付款->微信官方进行回调->支付成功。
2.准备材料
(1)首先要有公众号或者小程序或者app等,其次还需要注册一个微信商户。
(2)要去微信支付平台申请证书、秘钥等信息。
3.参数介绍
appId:小程序支付要使用,是小程序的appId。
appSecret:小程序支付使用的,是小程序的秘钥,生成后不可查看,记得保存。
merId:商家的商户号。
merchNumber:证书序列号。
apiV3Key:apiV3的秘钥。
3.注意事项
微信从24年8月份开始逐步取消平台私钥证书签名,新注册的商户号需要使用自己的公钥进行签名。
二、环境准备
微信支付分为V2和V3,本文主要介绍的是V3的支付流程。
项目采用SpringBoot。
1.设置开发参数
可以直接在yml文件中设置开发需要的参数
2.配置配置类
@Data
@Component
@ConfigurationProperties(prefix = "wx")
public class WxPayConfig {
//小程序公众号的appId
private String appId;
//小程序公众号的秘钥
private String appSecret;
//回调地址
private String notifyUrl;
//商家商户号
private String merchantId;
//私钥地址
private String privateKeyPath;
//公钥地址
private String publicKeyPath;
//证书序列号
private String merchantSerialNumber;
//apiv3的秘钥
private String apiV3Key;
}
二、使用官方sdk的方法
1.导入依赖
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.14</version>
</dependency>
2.预支付代码,这里用js支付举例
/**
* 预支付接口
*/
public void prepay(){
//更新证书配置,这里用官方sdk,不需要考虑时效性问题
Config config = new RSAPublicKeyConfig.Builder()
.merchantId(wxPayConfig.getMerchantId())
.privateKeyFromPath(wxPayConfig.getPrivateKeyPath())
.publicKeyFromPath(wxPayConfig.getPublicKeyPath())
.publicKeyId(wxPayConfig.getPublicKeyPath())
.merchantSerialNumber(wxPayConfig.getMerchantSerialNumber())
.apiV3Key(wxPayConfig.getApiV3Key())
.build();
//初始化service
JsapiService service = new JsapiService.Builder().config(config).build();
PrepayRequest request = new PrepayRequest();
request.setAppid(wxPayConfig.getAppId());
request.setMchid(wxPayConfig.getMerchantId());
request.setDescription("测试商品");
request.setOutTradeNo("orderId");
request.setNotifyUrl(wxPayConfig.getNotifyUrl());
log.info("支付回调地址:{}",wxPayConfig.getNotifyUrl());
Amount amount = new Amount();
amount.setTotal(1);
request.setAmount(amount);
Payer payer = new Payer();
request.setPayer(payer);
//调用预支付接口
PrepayResponse prepay = service.prepay(request);
预支付完成后会产生一个prepayId,返回给前端之后,前端根据返回参数拉起支付
3.前端签名
前端拉起支付有个参数是paySign,实际操作中前端不是很好处理,所以这里可以选择帮助前端进行签名。
public Map<String, String> getSign(String openId,Integer amount,String orderId) throws NoSuchAlgorithmException, IOException, InvalidKeyException, SignatureException {
//生成32位的随机字符串
String nonceStr = UUID.randomUUID().toString().replace("-", "");
long timestamp = System.currentTimeMillis() / 1000;
//prepay就是预支付代码,获取到prepayId
String prepayId = prepay(openId,amount,orderId);
String message = buildMessage(wxConfigVo.getAppId(), timestamp, nonceStr, prepayId);
Signature sign = Signature.getInstance(SHA256WITHRSA);
sign.initSign(getPrivateKey(wxConfigVo.getPrivateKeyPath()));
sign.update(message.getBytes(StandardCharsets.UTF_8));
String signature = Base64.getEncoder().encodeToString(sign.sign());
HashMap<String, String> map = new HashMap<>();
map.put("sign",signature);
map.put("nonceStr",nonceStr);
map.put("timestamp",timestamp+"");
map.put("prepayId",prepayId);
return map;
}
/**
* 拼接需要的信息
* @param appid
* @param timestamp
* @param nonceStr
* @param prepay_id
* @return
*/
String buildMessage(String appid, long timestamp,String nonceStr,String prepay_id) {
return appid + "\n"
+ timestamp + "\n"
+ nonceStr + "\n"
+ "prepay_id="+prepay_id + "\n";
}
/**
* 获取私钥文件
* @param filename
* @return
* @throws IOException
*/
public PrivateKey getPrivateKey(String filename) throws IOException {
String content = new String(Files.readAllBytes(Paths.get(filename)), StandardCharsets.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("无效的密钥格式");
}
}
在这里,前端也是需要我们签名产生的时间戳,随机字符串这些信息,所以选择返回一个map格式,里面都是重要信息。当然这一步选做,如果不想处理也可以选择让前端处理。这里只是列出方法,方便学习。
4.支付回调
拉起支付之后,用户选择付款之后,微信官方会发来一条支付完成的信息,里面包含是否支付成功等信息。
/**
* 支付回调
* @param request
* @param response
* @return
*/
public Transaction notifyPay(HttpServletRequest request, HttpServletResponse response) {
String signature = request.getHeader("Wechatpay-Signature");
String serial = request.getHeader("Wechatpay-Serial");
String nonce = request.getHeader("Wechatpay-Nonce");
String timestamp = request.getHeader("Wechatpay-Timestamp");
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(serial)
.nonce(nonce)
.signature(signature)
.timestamp(timestamp)
.body(getRequestBody(request))
.build();
NotificationConfig config = new RSAPublicKeyConfig.Builder()
.merchantId(wxConfigVo.getMerchantId())
.privateKeyFromPath(wxConfigVo.getPrivateKeyPath())
.publicKeyFromPath(wxConfigVo.getPublicKeyPath())
.publicKeyId(wxConfigVo.getPublicKey())
.merchantSerialNumber(wxConfigVo.getMerchantSerialNumber())
.apiV3Key(wxConfigVo.getApiV3Key())
.build();
NotificationParser parser = new NotificationParser(config);
Transaction transaction = parser.parse(requestParam, Transaction.class);
return transaction;
}
private String getRequestBody(HttpServletRequest request) {
StringBuilder sb = new StringBuilder();
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) {
}
return sb.toString();
}
5.注意事项
(1)在做支付的时候,遇到过一些问题,比如在加密解密的过程中,因为jdk版本过低导致加解密失败,这里可以去百度一下,替换一下jre下的security包下的两个包就可以解决,低版本最高支持128位加密,微信现在是256位的。
(2)新申请的商户号要用公钥签名,用公钥,用公钥。
(3)多看微信官方文档,这里只是列出了代码,具体的参数和签名,解密这些没有具体说明,后续会单独写一篇文章介绍所有加密解密,验签的方法和原理。
三、不使用官方sdk的方法
不是很推荐这样自己写代码,很复杂,而且容易出错,不过给爱研究的人看一下具体过程,这里只展示如何进行签名,具体调接口和参数根据自己实际业务来进行操作。
@Component
public class WechatUtil {
@Autowired
private WxPayConfig config;
private static String SHA256WITHRSA = "SHA256withRSA";
private String mchId= config.getMerchantId();
private String certificateSerialNo=config.getMerchantSerialNumber();
/**
* body是自己的支付信息,类似于{"appid":"wxd678efh567hg6787","mchid":"1230000109","description":"Image形象店-深圳腾大-QQ公仔","out_trade_no":"1217752501201407033233368018","notify_url":"https://www.weixin.qq.com/wxpay/pay.php","amount":{"total":100,"currency":"CNY"},"payer":{"openid":"oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"}}
* @param body
* @return
*/
public Map<String,String> getSignMap(String body) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException {
Map<String,String> map = new HashMap<>();
map.put("Authorization",getSign(body));
map.put("Content-Type","application/json");
map.put("Accept","application/json");
return map;
}
private String getSign(String body) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException {
return "WECHATPAY2-SHA256-RSA2048 "+getToken(body);
}
private String getToken(String body) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException {
String nonceStr = UUID.randomUUID().toString().replace("-", "");
String timestamp = System.currentTimeMillis() / 1000+"";
String message = buildMsg(timestamp,nonceStr,body);
Signature signature = Signature.getInstance(SHA256WITHRSA);
signature.initSign(getPrivateKey("自己文件路径"));
signature.update(message.getBytes(StandardCharsets.UTF_8));
String sign = Base64.getEncoder().encodeToString(signature.sign());
return "mchid=\"" + mchId + "\","
+ "nonce_str=\"" + nonceStr + "\","
+ "timestamp=\"" + timestamp + "\","
+ "serial_no=\"" + certificateSerialNo + "\","
+ "signature=\"" + sign + "\"";
}
private String buildMsg(String timestamp, String nonceStr, String body) {
return "POST"+"\n"
+"/v3/certificates"+"\n"
+timestamp+"\n"
+nonceStr+"\n"
+body+"\n";
}
/**
* 获取私钥文件
* @param filename
* @return
* @throws IOException
*/
public PrivateKey getPrivateKey(String filename) throws IOException {
String content = new String(Files.readAllBytes(Paths.get(filename)), StandardCharsets.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("无效的密钥格式");
}
}
第一个方法返回值就是微信拉起支付的请求头,body参数根据自己的业务来产生。
@RequestMapping(value = "/test", method = RequestMethod.POST, produces = "application/json; charset=utf-8")
@ApiOperation("测试")
public void test() throws Exception{
//处理请求参数
String param = JSON.toJSONString("请求参数");
//获取签名请求头
HashMap<String, String> heads = WechatUtil.getSignMap(param);
//请求微信接口
HttpUtils.requestPostBody("微信接口url", param, heads);
}
以上就是两种实现支付的方法,支付还需要考虑到很多业务问题,比如如何保证幂等性,回调如果失效需要主动查询微信方等等。关于RSA的内容和如何获取微信各种参数,申请证书这些后面会单独写一篇文章。