微信支付API v3:https://wechatpay-api.gitbook.io/wechatpay-api-v3
API字典:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/index.shtml
根据下面的流程,完成所有配置、准备后,分分钟唤起支付。
支持App、Jsapi、H5、Native,4种支付方式。
要调通微信支付最新的v3支付,需配置提供4项内容:商户私钥、证书序列号、APIv3密钥、平台证书。
登录(https://pay.weixin.qq.com)后,在“账户中心”->“API安全”。完成“申请API证书”和“设置APIv3密钥”配置。“设置API密钥”可不设置,v3支付用不到。
在“申请API证书”,根据引导流程完成配置,你将下载获得(apiclient_cert.p12、apiclient_cert.pem、apiclient_key.pem)三个文件,其中apiclient_key.pem就是商户私钥,另外两个文件没用。
在完成“申请API证书”配置后,右侧点击“管理证书”,可以看到证书序列号,复制保存下来。
随机字符串,对于v3支付,该密钥很重要。
4.1、https://github.com/EliasZzz/CertificateDownloader/releases,这里下载CertificateDownloader-1.1.jar包;
4.2、cmd执行命令:“java -jar CertificateDownloader-1.1.jar -f 商户私钥文件路径 -k 证书解密的密钥 -m 商户号 -o 证书保存路径 -s 商户证书序列号”。
商户私钥文件路径:apiclient_key.pem的本地存放路径;
证书解密的密钥:设置的APIv3密钥;
商户号:当前配置的商户号;
证书保存路径:生成平台证书后,存放的路径;
商户证书序列号:保存的证书序列号。
4.3、执行完后,在证书保存路径下,有个类似wechatpay_3B******27691.pem的文件,里面的内容就是平台证书。
在“产品中心”->“开发配置”。完成相关域名路径配置。
在“产品中心”->“AppID账号管理”。完成相关公众号、小程序绑定。(JSAPI支付需要)。
com.github.wechatpay-apiv3
wechatpay-apache-httpclient
0.2.2
/**
* 微信支付预下单-主体信息
* @author shuaifengjie.com
* v3
*/
@Data
public class WxpayTradeVo implements Serializable {
/**
*
*/
private static final long serialVersionUID = 8189139771687842957L;
// 由微信生成的应用ID,全局唯一。请求基础下单接口时请注意APPID的应用属性,应为公众号的APPID
// 示例值:wxd678efh567hg6787
private String appid;
// 直连商户的商户号,由微信支付生成并下发。
// 示例值:1230000109
private String mchid;
// 商品描述
// 示例值:Image形象店-深圳腾大-QQ公仔
private String description;
// 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
// 示例值:1217752501201407033233368018
private String out_trade_no;
// 通知URL必须为直接可访问的URL,不允许携带查询串。
// 示例值:https://www.weixin.qq.com/wxpay/pay.php
private String notify_url;
// 订单金额信息
private WxTradeAmountVo amount;
// 支付者信息 - JSAPI,必传
private WxPayerVo payer;
// 支付场景描述 - H5,必传
private WxSceneInfoVo scene_info;
public WxpayTradeVo() {
super();
}
public WxpayTradeVo(String appid, String mchid, String description, String out_trade_no, String notify_url) {
super();
this.appid = appid;
this.mchid = mchid;
this.description = description;
this.out_trade_no = out_trade_no;
this.notify_url = notify_url;
}
@Data
public static class WxTradeAmountVo {
// 订单总金额,单位为分。
// 示例值:100
private long total;
// CNY:人民币,境内商户号仅支持人民币。
// 示例值:CNY
private String currency = "CNY";
public WxTradeAmountVo() {
super();
}
public WxTradeAmountVo(long total) {
super();
this.total = total;
}
}
@Data
public static class WxPayerVo {
// 用户在直连商户appid下的唯一标识。 下单前需获取到用户的Openid,Openid获取详见
// 示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
private String openid;
public WxPayerVo() {
super();
}
public WxPayerVo(String openid) {
super();
this.openid = openid;
}
}
@Data
public static class WxSceneInfoVo {
// 用户的客户端IP,支持IPv4和IPv6两种格式的IP地址。
// 示例值:14.23.150.211
private String payer_client_ip;
// H5场景信息
private WxH5InfoVo h5_info;
public WxSceneInfoVo() {
super();
}
public WxSceneInfoVo(String payer_client_ip) {
super();
this.payer_client_ip = payer_client_ip;
}
}
@Data
public static class WxH5InfoVo {
// 场景类型
// 示例值:iOS, Android, Wap
private String type = "Wap";
}
}
/**
* 支付、退款数据回调通用
* @author shuaifengjie.com
*/
@Data
public class CallbackBodyVo implements Serializable {
/**
*
*/
private static final long serialVersionUID = -3279905156049535047L;
/** 通知Id */
private String id;
/**
* 通知创建时间
*/
private String create_time;
/**
* 通知类型
* 支付成功:TRANSACTION.SUCCESS
* 退款成功:REFUND.SUCCESS
*/
private String resource_type;
/**
* 通知数据类型
*/
private String event_type;
/**
* 回调摘要
*/
private String summary;
/**
* 通知数据
*/
private Resource resource;
/**
* 通知数据
*/
@Data
public static class Resource {
/**
* 对开启结果数据进行加密的加密算法,目前只支持AEAD_AES_256_GCM。
*/
private String algorithm;
/**
* Base64编码后的开启/停用结果数据密文。
*/
private String ciphertext;
/**
* 附加数据。
*/
private String associated_data;
/**
* 加密使用的随机串。
*/
private String nonce;
/**
* 原始回调类型。
*/
private String original_type;
}
}
/**
* 微信退款预下单-主体信息
* @author shuaifengjie.com
* v3
*/
@Data
public class WxpayRefundVo implements Serializable {
/**
*
*/
private static final long serialVersionUID = 8189139771687842957L;
// 原支付交易对应的微信订单号
// 示例值:1217752501201407033233368018
private String transaction_id;
// 若商户传入,会在下发给用户的退款消息中体现退款原因
// 示例值:商品已售完
private String reason;
// 商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。
// 示例值:1217752501201407033233368018
private String out_refund_no;
// 异步接收微信支付退款结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效,优先回调当前传的这个地址。
// 示例值:https://weixin.qq.com
private String notify_url;
// 订单金额信息
private WxRefundAmountVo amount;
public WxpayRefundVo() {
super();
}
@Data
public static class WxRefundAmountVo {
// 退款金额,币种的最小单位,只能为整数,不能超过原订单支付金额。
// 示例值:888
private long refund;
// 原支付交易的订单总金额,币种的最小单位,只能为整数。
// 示例值:888
private long total;
// CNY:人民币,境内商户号仅支持人民币。
// 示例值:CNY
private String currency = "CNY";
public WxRefundAmountVo() {
super();
}
public WxRefundAmountVo(long refund, long total) {
super();
this.refund = refund;
this.total = total;
}
}
}
private static String appid= "wxadf3******062eaa"; // appid(第“一”点,第5步,绑定的的公众号、小程序appid)
private static String mchid = "1xxxxxxx4"; // 商户号
private static String apiv3Key= "8af077******52de54c27"; // v3密钥
private static String mchSerialNo = "185xxxxxxxxxxxxxxxxx84D5"; // 商户私钥证书序列号
// 商户私钥(apiclient_key.pem文件,用文本编辑器打开复制进来就可以了)
private static String privateKey = "-----BEGIN PRIVATE KEY-----\n"
+ "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCx6p0IY6gvOa/S\n"
+ "..........\n"
+ "RhjzfVRMPMHXkBeZ60CH/oNG8dCFA96LRA4ZtdaBAoGAVdRPW71M9DcNOnQcXDHT\n"
+ "-----END PRIVATE KEY-----";
// 平台证书(类似wechatpay_3B4C48********2027691.pem的文件,文本编辑器打开复制)
private static String certificate = "-----BEGIN CERTIFICATE-----\n"
+ "MIID3DCCAsSgAwIBAgIUO0xIb2Q8Fm7S2M1BP82GdfICdpEwDQYJKoZIhvcNAQEL\n"
+ "..........\n"
+ "oDIMMb+IMVvRat8MiWRuWEe6OjSZmeeNatCV0pcyers=\n"
+ "-----END CERTIFICATE-----";
如果出现“应答的微信支付签名验证失败”,请检查平台证书(certificate)。该证书是第“一”点的第4步获取的证书。
PayScene:app/h5/pc/wx;
ClientException:自定义异常,删掉即可;
StringHelper:String转Map参数方法;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.X509Certificate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.stream.Collectors;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.util.Base64Utils;
import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.extern.slf4j.Slf4j;
/**
* 微信支付帮助类
* @author shuaifengjie.com
*/
@Slf4j
public class WxpayUtils {
/**
* 发起支付
* @param tradeVo
* @param scene
* @param privateKey
* @param certificate
* @param serialNo
* @return
*/
public static ObjectNode getWxpay(WxpayTradeVo tradeVo, String scene, String privateKey, String certificate, String serialNo) {
// ...
String responseBody = response(JSON.toJSONString(tradeVo), getUrl(scene), tradeVo.getMchid(), privateKey, certificate, serialNo);
log.info("======>>> wxpay response body: ", responseBody);
// parse
@SuppressWarnings("unchecked")
Map resultMap = StringHelper.strToMap(responseBody);
// ...
ObjectMapper objectMapper = new ObjectMapper();
ObjectNode objectNode = objectMapper.createObjectNode();
// ...
if (PayScene.WEB.getScene().equals(scene)) {// pc - 扫码支付(Native)
objectNode.put("code_url", resultMap.get("code_url"));
} else if (PayScene.H5.getScene().equals(scene)) {// 手机网页(H5)
objectNode.put("h5_url", resultMap.get("h5_url"));
} else if (PayScene.WX_H5.getScene().equals(scene)) {// 微信公众号(H5)
String prepayId = resultMap.get("prepay_id");
objectNode.put("prepay_id", prepayId);
// ...
objectNode.put("appId", tradeVo.getAppid());
String timestamp = getTimestamp();
objectNode.put("timeStamp", timestamp);
String nonceStr = CreateNoncestr();
objectNode.put("nonceStr", nonceStr);
String packageStr = "prepay_id=" + prepayId;
objectNode.put("package", packageStr);
// 签名
String paySign = doRequestSign(privateKey, tradeVo.getAppid(), timestamp, nonceStr, packageStr);
objectNode.put("paySign", paySign);
objectNode.put("signType", "RSA");
}
return objectNode;
}
/**
* 发起退款
* @param tradeVo
* @param mchid
* @param privateKey
* @param certificate
* @param serialNo
* @return
*/
public static void refund(WxpayRefundVo tradeVo, String mchid, String privateKey, String certificate, String serialNo) {
// ...
String responseBody = response(JSON.toJSONString(tradeVo), REFUND_URL, mchid, privateKey, certificate, serialNo);
log.info("======>>> wxrefund response body: ", responseBody);
}
/**
* 公共请求
* @param data
* @param url
* @param mchid
* @param privateKey
* @param certificate
* @param serialNo
* @return
*/
public static String response(String data, String url, String mchid, String privateKey, String certificate, String serialNo) {
log.info("====>>> wxpay response, mchid: {}, serialNo: {}, url: {}, privateKey: {}, certificate: {}, data: {}",
mchid, serialNo, url, privateKey, certificate, data);
// ...
HttpPost httpPost = new HttpPost(url);
StringEntity reqEntity = new StringEntity(data, ContentType.create("application/json", "utf-8"));
// ...
httpPost.setEntity(reqEntity);
httpPost.addHeader("Accept", "application/json");
// ...
CloseableHttpResponse response = null;
// 返回
String body = null;
try {
// 商户私钥
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8")));
// ...
response = getClient(mchid, serialNo, merchantPrivateKey, certificate).execute(httpPost);
log.info("====>>> wxpay response, result: {}", JSON.toJSONString(response));
// response = httpClient.execute(httpPost);
// assertTrue(response.getStatusLine().getStatusCode() != 401);
int statusCode = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
body = EntityUtils.toString(entity);
log.info("====>>> wxpay response, success: {}, body: {}", statusCode, body);
if (statusCode != 200) {
@SuppressWarnings("unchecked")
Map errorMsg = StringHelper.strToMap(body);
throw new ClientException(errorMsg.get("message"), ClientExceptionEnum.SERVICE_FORBIDDEN.getCode());
}
// 执行释放
EntityUtils.consume(entity);
} catch (ClientException e) {
log.error("======>>> wxpay response, client exception: {}", e);
throw new ClientException(e.getMessage(), e.getCode());
} catch (Exception e) {
log.error("======>>> wxpay response, exception: {}", e);
throw new ClientException("微信请求失败,请重试(1)", ClientExceptionEnum.SERVICE_FORBIDDEN.getCode());
} finally {
try {
response.close();
} catch (IOException e) {
log.error("======>>> wxpay response, ioe exception: {}", e);
throw new ClientException("微信请求失败,请重试(2)", ClientExceptionEnum.SERVICE_FORBIDDEN.getCode());
}
}
return body;
}
/**
* 创建Client
* @param mchid
* @param serialNo
* @param privateKey
* @param certificate
* @return
* @throws UnsupportedEncodingException
*/
public static CloseableHttpClient getClient(String mchid, String serialNo, PrivateKey privateKey, String certificate) throws UnsupportedEncodingException {
// 使用自动更新的签名验证器,不需要传入证书
// AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
// new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)), apiV3Key.getBytes("utf-8"));
// builder
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create().withMerchant(mchid, serialNo, privateKey);
// .withValidator(new WechatPay2Validator(verifier));
List certs = new ArrayList<>();
certs.add(PemUtil.loadCertificate(new ByteArrayInputStream(certificate.getBytes("utf-8"))));
builder.withWechatpay(certs);
return builder.build();
}
private static final String JSAPI_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
private static final String H5_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/h5";
private static final String NATIVE_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/native";
private static final String REFUND_URL = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";
/**
*
* @param scene
* @return
*/
private static String getUrl(String scene) {
// ...
if (PayScene.WEB.getScene().equals(scene)) {// pc - 扫码支付(Native)
return NATIVE_URL;
} else if (PayScene.H5.getScene().equals(scene)) {// 手机网页(H5)
return H5_URL;
} else if (PayScene.WX_H5.getScene().equals(scene)) {// 微信公众号(H5)
return JSAPI_URL;
} else {
throw new ClientException("不支持的[" + scene + "]场景", ClientExceptionEnum.DATA_MATCH_FAIL.getCode());
}
}
/**
* 字符串
* @return
*/
public static String CreateNoncestr() {
String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
String res = "";
for (int i = 0; i < 16; i++) {
Random rd = new Random();
res += chars.charAt(rd.nextInt(chars.length() - 1));
}
return res;
}
/**
* 得到时间戳
* @return
*/
public static String getTimestamp() {
long epochSecond = LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
return String.valueOf(epochSecond);
// return Long.toString(System.currentTimeMillis() / 1000);
}
/**
* 加密算法提供方 - BouncyCastle
*/
private static final String BC_PROVIDER = "BC";
/**
*
* @param privateKey
* @param orderedComponents
* @return
*/
public static String doRequestSign(String privateKey, String... orderedComponents) {
try {
// 商户私钥
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8")));
Signature signer = Signature.getInstance("SHA256withRSA", BC_PROVIDER);
signer.initSign(merchantPrivateKey);
final String signatureStr = createSign(true, orderedComponents);
signer.update(signatureStr.getBytes(StandardCharsets.UTF_8));
return Base64Utils.encodeToString(signer.sign());
} catch (InvalidKeyException e) {
log.error("InvalidKeyException: {}", e);
} catch (SignatureException e) {
log.error("SignatureException: {}", e);
} catch (NoSuchProviderException e) {
log.error("NoSuchProviderException: {}", e);
} catch (NoSuchAlgorithmException e) {
log.error("NoSuchAlgorithmException: {}", e);
} catch (UnsupportedEncodingException e) {
log.error("UnsupportedEncodingException: {}", e);
}
return null;
}
/**
* 请求时设置签名 组件
* @param components the components
* @return string string
*/
private static String createSign(boolean newLine, String... components) {
String suffix = newLine ? "\n" : "";
return Arrays.stream(components).collect(Collectors.joining("\n", "", suffix));
}
/**
* 使用微信平台证书,对微信回调数据验签,做应答签名比较
* @param certificate
* @param wechatpaySignature
* @param wechatpayTimestamp
* @param wechatpayNonce
* @param body
* @return
*/
public static boolean responseSignVerify(String certificate, String wechatpaySignature, String wechatpayTimestamp, String wechatpayNonce, String body) {
try {
final String signatureStr = createSign(true, wechatpayTimestamp, wechatpayNonce, body);
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initVerify(PemUtil.loadCertificate(new ByteArrayInputStream(certificate.getBytes("utf-8"))));
signer.update(signatureStr.getBytes(StandardCharsets.UTF_8));
// ...
return signer.verify(Base64Utils.decodeFromString(wechatpaySignature));
} catch (UnsupportedEncodingException e) {
log.error("UnsupportedEncodingException: {}", e);
} catch (SignatureException e) {
log.error("SignatureException: {}", e);
} catch (NoSuchAlgorithmException e) {
log.error("NoSuchAlgorithmException: {}", e);
} catch (InvalidKeyException e) {
log.error("InvalidKeyException: {}", e);
}
return false;
}
/**
* 微信V3密钥解密响应体
* @param apiv3Key
* @param bodyVo
* @return
*/
public static String decryptResponseBody(String apiv3Key, CallbackBodyVo bodyVo) {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec key = new SecretKeySpec(apiv3Key.getBytes(StandardCharsets.UTF_8), "AES");
GCMParameterSpec spec = new GCMParameterSpec(128, bodyVo.getResource().getNonce().getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD(bodyVo.getResource().getAssociated_data().getBytes(StandardCharsets.UTF_8));
byte[] bytes = cipher.doFinal(Base64Utils.decodeFromString(bodyVo.getResource().getCiphertext()));
return new String(bytes, StandardCharsets.UTF_8);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
log.error("NoSuchAlgorithmException: {}", e);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
log.error("InvalidKeyException: {}", e);
} catch (IllegalBlockSizeException e) {
log.error("IllegalBlockSizeException: {}", e);
} catch (BadPaddingException e) {
log.error("BadPaddingException: {}", e);
}
return null;
}
}
// 主体
WxpayTradeVo tradeVo = new WxpayTradeVo();
tradeVo.setAppid("appid");
tradeVo.setMchid("mchid" );
tradeVo.setNotify_url("notifyUrl");
tradeVo.setOut_trade_no("outTradeNo");
tradeVo.setDescription("descr");
// amount
tradeVo.setAmount(new WxTradeAmountVo(1));
// 场景
tradeVo.setScene_info(new WxSceneInfoVo("115.28.90.28"));
// 发起支付的场景:app、h5、pc、wx
String scene = "wx";
// 微信公众号(H5),需要openid
if ("wx".equals(scene)) {
tradeVo.setPayer(new WxPayerVo(openid));
}
// ...
ObjectNode objectNode = WxpayUtils.getWxpay(tradeVo, scene, privateKey, publicKey, paymentMchDto.getSerialNo());