简介:
微信支付的文档就不吐槽了,记录下微信支付,小程序支付的实现
开发前准备
账号申请,公钥私钥啥的去官网开发指引-小程序支付 | 微信支付商户平台文档中心 (qq.com)
核心代码
下单及拉起支付
微信支付的所有接口调用都是差不多的,关键在于如何处理网络请求和签名,直接上代码
网络请求我用的是okhttp,以小程序支付为例
/**
* 微信小程序支付下单
* 商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易会话标识后再按Native、JSAPI、APP等不同场景生成交易串调起支付。
* 请求URL:https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi
* 请求方式:POST
*
* @param dto 微信小程序订单参数
* @param merchantInfo 商户号信息
* @return
*/
public static ResultData createWeChatAppletPayOrder(WeChatAppletPlaceOrderDto dto, MerchantInfo merchantInfo) {
WeChatAppletPlaceOrder param = new WeChatAppletPlaceOrder();
BeanUtils.copyBeanProp(param, dto);
String reqData = JSON.toJSONString(param);
log.info("创建微信小程序订单参数:{}", reqData);
try {
String token = WxApiV3SignUtils.getToken("POST", HttpUrl.parse(dto.getUrl()),
reqData, merchantInfo.getMchId(), merchantInfo.getPrivateKey(), merchantInfo.getSerialNo());
OkHttpClient okHttpClient = new OkHttpClient();
RequestBody requestBodyJson = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqData);
Request request = new Request.Builder()
.url(dto.getUrl())
.addHeader("Accept", "*/*")
.addHeader("Authorization", token)
.post(requestBodyJson)
.build();
Call call = okHttpClient.newCall(request);
Response response = call.execute();
String resStr = response.body().string();
log.info("创建微信小程序订单返回结果:状态码:{}--{}", response.code() ,resStr);
//响应成功
if (response.isSuccessful()) {
JSONObject json = JSON.parseObject(resStr);
WeChatAppletPlaceOrderVO vo = new WeChatAppletPlaceOrderVO();
vo.setPrepayId("prepay_id=" + json.getString("prepay_id"));
log.info("prepay_id=" + json.getString("prepay_id"));
/*小程序调起支付签名信息*/
WeChatAppletRequestPaymentVO paymentVO = WxApiV3SignUtils.getSign(dto.getAppId(),"prepay_id=" + json.getString("prepay_id"), merchantInfo.getPrivateKey());
vo.setTimeStamp(String.valueOf(paymentVO.getTimestamp()));
log.info(String.valueOf(paymentVO.getTimestamp()));
vo.setNonceStr(paymentVO.getNonceStr());
log.info(paymentVO.getNonceStr());
vo.setPaySign(paymentVO.getSignature());
log.info(paymentVO.getSignature());
vo.setPrepayId("prepay_id=" + json.getString("prepay_id"));
vo.setSignType("RSA");
return ResultData.success("success", vo);
} else {
return ResultData.error(response.code(), response.body().string());
}
} catch (Exception e) {
log.error("创建微信小程序订单出错:" + e.getMessage());
return ResultData.error(e.getMessage());
}
}
请求参数和返回参数按照接口文档封装就好了,讲一下token和返给前端拉起支付的签名,其实官方也有相关的代码示例,下面的getToken是所有微信支付相关请求获取token通用的方法,sign方法也是,下单和拉起支付的签名方式是一样的,只是签名的参数不同,
微信小程序支付的签名参数是:请求参数是请求方式method,小程序下单接口的url,时间戳timestamp,随机字符串nonceStr,请求参数body。
微信小程序拉起支付的参数是:小程序appId,时间戳timestamp,随机字符串nonceStr,下单成功返回的prepayId(小程序签名要在prepayId前拼接上"prepay_id=")
package com.carlos.pay.common.util;
import com.alibaba.fastjson.JSONObject;
import com.carlos.pay.doamin.vo.WeChatAppletRequestPaymentVO;
import lombok.extern.slf4j.Slf4j;
import okhttp3.HttpUrl;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.FileCopyUtils;
import sun.misc.BASE64Decoder;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Random;
/**
* @author carlos
*/
@Slf4j
public class WxApiV3SignUtils {
private static final String schema = "WECHATPAY2-SHA256-RSA2048";
private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final Random RANDOM = new SecureRandom();
/**
* 签名,生成token
*
* @param method
* @param url
* @param body
* @param mchId 商户号
* @param privateKey 商户证书私钥
* @param serialNo 商户API证书序列号
* @return
* @throws Exception
*/
public static String getToken(String method, HttpUrl url, String body, String mchId, String privateKey, String serialNo) throws Exception {
String nonceStr = generateNonceStr();
log.info("nonceStr: " + nonceStr);
long timestamp = System.currentTimeMillis() / 1000;
log.info("timestamp: " + timestamp);
String message = buildMessage(method, url, timestamp, nonceStr, body);
String signature = sign(message.getBytes("utf-8"), privateKey);
log.info("signature: " + signature);
return schema + " " + "mchid=\"" + mchId + "\","
+ "nonce_str=\"" + nonceStr + "\","
+ "timestamp=\"" + timestamp + "\","
+ "serial_no=\"" + serialNo + "\","
+ "signature=\"" + signature + "\"";
}
/**
* 拉起支付签名返给前端拉起支付
*
* @param appId
* @param prepayId
* @param privateKey
* @return
* @throws Exception
*/
public static WeChatAppletRequestPaymentVO getSign(String appId, String prepayId, String privateKey) throws Exception {
String nonceStr = generateNonceStr();
log.info("nonceStr: " + nonceStr);
long timestamp = System.currentTimeMillis() / 1000;
log.info("timestamp: " + timestamp);
String message = appId + "\n" + timestamp + "\n" + nonceStr + "\n" + prepayId + "\n";
String signature = sign(message.getBytes("utf-8"), privateKey);
log.info("signature: " + signature);
return new WeChatAppletRequestPaymentVO(nonceStr, timestamp, signature);
}
/**
* @param message
* @param privateKey 商户私钥
* @return
*/
private static String sign(byte[] message, String privateKey) throws Exception {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(getPrivateKey(privateKey));
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
}
private static 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";
}
/**
* String转私钥PrivateKey
*
* @param key
* @return
* @throws Exception
*/
public static PrivateKey getPrivateKey(String key) throws Exception {
byte[] keyBytes;
keyBytes = (new BASE64Decoder()).decodeBuffer(key);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
}
/**
* String转私钥PrivateKey 从pem文件中获取私钥
*
* @param fileName
* @return
* @throws Exception
*/
public static String getPrivateKeyByFile(String fileName) throws Exception {
ClassPathResource cpr = new ClassPathResource(fileName);
String content = new String(FileCopyUtils.copyToByteArray(cpr.getInputStream()), "utf-8");
String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
return privateKey;
}
/**
* 获取随机字符串 Nonce Str
*
* @return String 随机字符串
*/
public static String generateNonceStr() {
char[] nonceChars = new char[32];
for (int index = 0; index < nonceChars.length; ++index) {
nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
}
return new String(nonceChars);
}
/**
* V3回调解密
*
* @param body
* @param keyV3
* @return
* @throws IOException
* @throws GeneralSecurityException
*/
public static String decryptBody(String body, String keyV3) throws IOException, GeneralSecurityException {
AesUtil aesUtil = new AesUtil(keyV3.getBytes("utf-8"));
JSONObject object = JSONObject.parseObject(body);
JSONObject resource = object.getJSONObject("resource");
String ciphertext = resource.getString("ciphertext");
String associatedData = resource.getString("associated_data");
String nonce = resource.getString("nonce");
log.info("回调解密参数--ciphertext:" + ciphertext);
log.info("回调解密参数--associatedData:" + associatedData);
log.info("回调解密参数--nonce:" + nonce);
return aesUtil.decryptToString(associatedData.getBytes("utf-8"), nonce.getBytes("utf-8"), ciphertext);
}
}
对了上面的getPrivateKey和getPrivateKeyByFile都是用来获取PrivateKey的,一个是从String转的,一个是读pem文件的,我的pem文件是放在resources下的。
好了,下单拉起支付就到这了。
回调
回调没什么好说的,返回的是加密的报文,用上文的WxApiV3SignUtils.decryptBody方法解密据OK啦
上代码咯
/**
* 微信小程序支付回调v3
*
* @param request
* @param response
* @return
* @throws IOException
* @throws GeneralSecurityException
*/
@Override
public synchronized Map<String, String> wxMiniPayCallBackV3(HttpServletRequest request, HttpServletResponse response) throws IOException, GeneralSecurityException {
log.info("微信小程序支付回调开始");
String line = request.getReader().readLine();
request.getReader().close();
log.info("接收到的报文:" + line);
String result = WxApiV3SignUtils.decryptBody(line);
log.info("解密后的报文:" + result);
//将字符串转换成json
JSONObject json = JSONObject.parseObject(result);
Map<String, String> map = new HashMap<>();
try {
//TODO 你的业务 boolean flag = 业务处理的成功失败
} catch (Exception e) {
//响应微信
map.put("code", "FAIL");
map.put("message", "失败");
log.info("回复微信消息:FAIL");
return map;
}
if (flag) {
//响应微信
map.put("code", "SUCCESS");
map.put("message", "成功");
log.info("回复微信消息:SUCCESS");
return map;
}
//响应微信
map.put("code", "FAIL");
map.put("message", "失败");
log.info("回复微信消息:FAIL");
return map;
}
总结
微信支付相关的东西其实并不难,其实核心就是WxApiV3SignUtils里面的东西,签名,加解密。