对于微信支付V3版本,相信大家都不陌生,官方文档很笼统,demo也是无从下手,需要先过一遍demo源码,理解业务,才能进行下一步的开发,并且中间坑也不少,许多刚接触的人,都会感觉无从下手的感觉。所以今天出一版java接入微信支付V3版本整合版。
对于安全基础设施部分的工作,初次接入的小伙伴恐怕会是无从下手,那就介绍一下,微信支付V3的安全验签和签名登操作。
com.github.wechatpay-apiv3
wechatpay-apache-httpclient
0.4.7
public class AesUtil {
static final int KEY_LENGTH_BYTE = 32;
static final int TAG_LENGTH_BIT = 128;
private final byte[] aesKey;
/**
* @param key APIv3 密钥
*/
public AesUtil(byte[] key) {
if (key.length != KEY_LENGTH_BYTE) {
throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
}
this.aesKey = key;
}
/**
* 证书和回调报文解密
*
* @param associatedData associated_data
* @param nonce nonce
* @param cipherText ciphertext
* @return {String} 平台证书明文
* @throws GeneralSecurityException 异常
*/
public String decryptToString(byte[] associatedData, byte[] nonce, String cipherText) throws GeneralSecurityException {
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);
return new String(cipher.doFinal(Base64.decode(cipherText)), StandardCharsets.UTF_8);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException(e);
}
}
}
public enum WxPayEnum {
//app支付
APP_PAY("v3/pay/transactions/app"),
//查询订单
QUERY_PAY("v3/pay/transactions/id/"),
//关闭订单
CLOSE_PAY("v3/pay/transactions/out-trade-no/{out_trade_no}/close"),
//申请退款
REFUNDS_PAY("v3/refund/domestic/refunds"),
//查询单笔退款
QUERY_REFUNDS_PAY("v3/refund/domestic/refunds/"),
//申请交易账单
BILL_PAY("v3/bill/tradebill"),
//申请资金账单
FUNDFLOWBILL_PAY("v3/bill/fundflowbill");
private String desc;
WxPayEnum(String desc) {
this.desc = desc;
}
public String getDesc() {
return desc;
}
}
public class WxPayGet {
//请求网关
private static final String url_prex = "https://api.mch.weixin.qq.com/";
//APP支付
private static final String url = WxPayEnum.APP_PAY.getDesc();
//编码
private static final String charset = "UTF-8";
public static String WxPayGet(String jsonStr, String merchId, String serial_no, String privateKeyFilePath) throws Exception {
String body = "";
CloseableHttpClient client = HttpClients.createDefault();
HttpPost post = new HttpPost(url_prex + url);
StringEntity s = new StringEntity(jsonStr, charset);
s.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE, "application/json"));
//设置参数到请求体中去
post.setEntity(s);
String token = new GetToken().getToken("POST", HttpUrl.parse(url_prex + url), merchId, serial_no, privateKeyFilePath, jsonStr);
post.setHeader("Content-Type", "application/json");
post.setHeader("Accept", "application/json");
post.setHeader("Authorization",
"WECHATPAY2-SHA256-RSA2048 " + token);
//执行请求操作,并拿到结果(同步阻塞)
CloseableHttpResponse response = client.execute(post);
HttpEntity entity = response.getEntity();
if (entity != null) {
//按指定编码转换结果实体为String类型
body = EntityUtils.toString(entity, charset);
}
EntityUtils.consume(entity);
response.close();
return JSON.parseObject(body).get("prepay_id").toString();
}
}
/**
* 微信调起支付参数
* 返回参数如有不理解 请访问微信官方文档
* https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_1_4.shtml
*
* @param prepayId 微信下单返回的prepay_id
* @param appId 应用ID(appid)
* @param privateKeyFilePath 私钥的地址
* @return 当前调起支付所需的参数
* @throws Exception
*/
public static JSONObject WxTuneUp(String prepayId, String appId, String privateKeyFilePath) throws Exception {
String time = System.currentTimeMillis() / 1000 + "";
String nonceStr = UUID.randomUUID().toString().replace("-", "");
String packageStr = "prepay_id=" + prepayId;
ArrayList<String> list = new ArrayList<>();
list.add(appId);
list.add(time);
list.add(nonceStr);
list.add(packageStr);
//加载签名
String packageSign = sign(buildSignMessage(list).getBytes(), privateKeyFilePath);
JSONObject jsonObject = new JSONObject();
jsonObject.put("appid", appId);
jsonObject.put("timeStamp", time);
jsonObject.put("nonceStr", nonceStr);
jsonObject.put("packages", packageStr);
jsonObject.put("signType", "RSA");
jsonObject.put("paySign", packageSign);
return jsonObject;
}
/**
* 处理微信异步回调
*
* @param request
* @param response
* @param privateKey 32的秘钥
*/
public static String notify(HttpServletRequest request, HttpServletResponse response, String privateKey) throws Exception {
Map<String, String> map = new HashMap<>(12);
String result = readData(request);
// 需要通过证书序列号查找对应的证书,verifyNotify 中有验证证书的序列号
String plainText = verifyNotify(result, privateKey);
if (StrUtil.isNotEmpty(plainText)) {
response.setStatus(200);
map.put("code", "SUCCESS");
map.put("message", "SUCCESS");
} else {
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "签名错误");
}
response.setHeader("Content-type", ContentType.JSON.toString());
response.getOutputStream().write(JSONUtil.toJsonStr(map).getBytes(StandardCharsets.UTF_8));
response.flushBuffer();
String out_trade_no = JSONObject.fromObject(plainText).getString("out_trade_no");
return out_trade_no;
}
//请求头token
public String getToken(String method, HttpUrl url, String merchId, String serial_no, String privateKeyFilePath, String body) throws Exception {
String nonceStr = generateNonceStr();
long timestamp = System.currentTimeMillis() / 1000;
String message = buildMessage(method, url, timestamp, nonceStr, body);
String signature = sign(message.getBytes("UTF-8"), privateKeyFilePath);
String token = "merchId=\"" + merchId + "\","
+ "nonce_str=\"" + nonceStr + "\","
+ "timestamp=\"" + timestamp + "\","
+ "serial_no=\"" + serial_no + "\","
+ "signature=\"" + signature + "\"";
log.info("authorization token=[{}]", token);
return token;
}
//生成签名
static String sign(byte[] message, String privateKeyFilePath) {
try {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(getPrivateKey(privateKeyFilePath));
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
} catch (SignatureException | IOException e) {
throw new RuntimeException("签名验证过程发生了错误", e);
} catch (InvalidKeyException e) {
throw new RuntimeException("无效的证书", e);
}
}
//组装签名加载 buildMessage
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";
}
//获取私钥
public static PrivateKey getPrivateKey(String filename) throws IOException {
String content = new String(Files.readAllBytes(Paths.get(filename)), "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("无效的密钥格式");
}
}
/**
* 构造签名串
*
* @param signMessage 待签名的参数
* @return 构造后带待签名串
*/
static String buildSignMessage(ArrayList<String> signMessage) {
if (signMessage == null || signMessage.size() <= 0) {
return null;
}
StringBuilder sbf = new StringBuilder();
for (String str : signMessage) {
sbf.append(str).append("\n");
}
return sbf.toString();
}
/**
* v3 支付异步通知验证签名
*
* @param body 异步通知密文
* @param key api 密钥
* @return 异步通知明文
* @throws Exception 异常信息
*/
static String verifyNotify(String body, String key) throws Exception {
// 获取平台证书序列号
cn.hutool.json.JSONObject resultObject = JSONUtil.parseObj(body);
cn.hutool.json.JSONObject resource = resultObject.getJSONObject("resource");
String cipherText = resource.getStr("ciphertext");
String nonceStr = resource.getStr("nonce");
String associatedData = resource.getStr("associated_data");
AesUtil aesUtil = new AesUtil(key.getBytes(StandardCharsets.UTF_8));
// 密文解密
return aesUtil.decryptToString(
associatedData.getBytes(StandardCharsets.UTF_8),
nonceStr.getBytes(StandardCharsets.UTF_8),
cipherText
);
}
/**
* 处理返回对象
*
* @param request
* @return
*/
public static String readData(HttpServletRequest request) {
BufferedReader br = null;
try {
StringBuilder result = new StringBuilder();
br = request.getReader();
for (String line; (line = br.readLine()) != null; ) {
if (result.length() > 0) {
result.append("\n");
}
result.append(line);
}
return result.toString();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
最后就是业务上的接口调用了,比如下单、退单,等情况。