- 初识微信支付(不明白微信文档的请看,否则请跳过)
- 指引文档
- API字典
- 接口规则
- 具体流程+代码
- 第一步:申请证书并创建应用
- 第二步:下单支付(熟悉调用流程、封装参数配置类、公用支付方法)
- 第三步:接收异步通知
- 结束总结:
微信文档地址
众所周知,看别人的文档是个痛苦的事情,尤其是复杂的文档,这里说一下自己总结看文档的经验,先不着急上去就看某一个功能,先把文档的分类搞明白,然后是每个分类下的每个功能列表,切忌直接进去一顿操作跳转,结果自己都晕了,感觉到处都是支付的信息,总结一句话,看文档之前,先搞懂写文档人的套路,然后你会发现,真的是手到擒来,废话不多说,直接进入正题。
第一步,进入文档中心
主要看这几个,最重要的是API字典!
这里主要是告诉我们接入微信支付的前置条件以及满足接入条件之后的开发步骤。这里建议大概看一下。
- 接入前的准备
主要是准备一些支付所需要的资料,例如:appid,证书等,基本按照文档一步步操作就能轻松搞定。 - 开发指引
这里主要是讲开发步骤,以及提供一些各语言的参考代码,建议主看,初看的时候尽量先看一遍流程,先不要点里边的链接跳转过去看,搞明白了之后再回头逐步去研究每个步骤的详细介绍。
总结一下,简单的流程:
1、前端请求后端支付接口(一般给个订单号即可)
2、后端加载证书,配置预支付所需要的参数
3、请求微信支付下单接口,获取预支付id,也就是所谓的 prepay_id
4、组装好前端拉起微信支付所需要的参数,返回给前端即可
5、前端拉起微信支付,支付或取消,微信会以异步通知告知后端,这里需要后端提供一个接口给微信支付官方(文档说是必须用https,我用的http也可以)
6、接收微信异步通知,获取相关参数,处理对应逻辑
详细流程及代码逻辑,下边会具体说!!!
这个比较简单但也是最重要的,后边我们会频繁用到
这个就不多说了,提醒一下,请求接口的时候一定要正确填写接口地址和请求方式!!!
这里主要讲签名,验签,敏感信息加解密(例如:异步通知返回的订单信息),证书更新(微信支付证书有时间限制,需要定期更新)的介绍
准备必要的材料,去微信开放平台-管理中心,创建移动应用(应用没有上线也可以申请,资料填好就行,没有的先不填,基本可以通过审核),之后申请开通微信支付功能,得到所有配置所需要的参数。
注意,这里有个很容易配错的地方:应用签名,创建app应用的时候会让你填写应用签名和应用包名,应用包名是你自己填写的,保持跟安卓包名一致即可,应用签名并不是你keystore文件里的MD5值,而是需要你用微信提供的工具安装到手机上(同时安装你的app),获取你app的签名。
如果配错应用签名,你会发现app只能首次拉起微信支付,之后支付的时候,微信怎么都拉不起来!!!
下载证书,配置微信支付。
证书申请参考:证书申请
配置微信支付config类:
public class WxPayInfoConfig {
public final static String mchId = "********";
public final static String appid = "********";
public final static String apiV3Key = "********";
public final static String refund_notify_url = "http://api.wx.com/refundNotify/refunded/wxNotify";
public final static String pay_notify_url = "http://api.wx.com/payNotify/payed/wxNotify";
public final static String privateKeyPath = "wx/apiclient_key.pem";
public final static String singleUrl = "https://api.mch.weixin.qq.com/v3/pay/transactions/app";
public final static String refundUrl = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";
public final static String queryUrl = "https://api.mch.weixin.qq.com/v3/pay/transactions/id/%s?mchid=%s";
public final static String closeUrl = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/%s/close";
public static String mchSerialNo;
public static PrivateKey privateKey;
static {
// 加载商户私钥(privateKey:私钥字符串)
privateKey = WxPayUtil.getPrivateKey(privateKeyPath);
// 加载平台证书(mchId:商户号,mchSerialNo:商户证书序列号,apiV3Key:V3密钥)
X509Certificate certificate = WxPayUtil.getX509Certificate();
// 证书的序列号 也有用
mchSerialNo = certificate.getSerialNumber().toString(16).toUpperCase();
}
}
提取微信支付工具类:
这里注意,微信证书会过期,所以需要你定期更新证书,可手动更新,也可以自动更新,这里说下自动更新,自动更新,微信提供了java库wechatpay-apache-httpclient(不仅封装了证书相关发放,还封装了下单,加解密的相关方法,可以细细研究一下)
在我们往下看之前,请务必了解微信支付请求流程:
以下单支付流程为例:
com.github.wechatpay-apiv3 wechatpay-apache-httpclient 0.2.3
详细介绍
工具类配置
public class WxPayUtil {
private static final ConcurrentHashMap verifierMap = new ConcurrentHashMap<>();
static AutoUpdateCertificatesVerifier getVerifier(String serialNumber) {
AutoUpdateCertificatesVerifier verifier = null;
if (verifierMap.isEmpty() || !verifierMap.containsKey(serialNumber)) {
verifierMap.clear();
try {
//刷新
PrivateKeySigner signer = new PrivateKeySigner(WxPayInfoConfig.mchSerialNo, WxPayInfoConfig.privateKey);
WechatPay2Credentials credentials = new WechatPay2Credentials(WxPayInfoConfig.mchId, signer);
verifier = new AutoUpdateCertificatesVerifier(credentials
, WxPayInfoConfig.apiV3Key.getBytes("utf-8"));
verifierMap.put(verifier.getValidCertificate().getSerialNumber()+"", verifier);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
} else {
verifier = verifierMap.get(serialNumber);
}
return verifier;
}
static CloseableHttpClient getHttpClient(String serialNumber) {
//获取平台证书
AutoUpdateCertificatesVerifier verifier = getVerifier(serialNumber);
if (verifier == null) {
ExceptionCast.cast(CommonCode.OPERATION_ERROR);
}
// 初始化httpClient httpClient会在发送请求之前拦截并且自动根据请求信息创建签名
CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create()
.withMerchant(WxPayInfoConfig.mchId, WxPayInfoConfig.mchSerialNo, WxPayInfoConfig.privateKey)
.withValidator(new WechatPay2Validator(verifier)).build();
return httpClient;
}
public static PrivateKey getPrivateKey(String fileName) {
try {
ClassPathResource resource = new ClassPathResource(fileName);
String content = IOUtils.toString(resource.getInputStream(), "utf-8");
String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\s+", "");
KeyFactory kf = KeyFactory.getInstance("RSA");
PrivateKey key = kf.generatePrivate(new PKCS8EncodedKeySpec(base64.getDecoder().decode(privateKey)));
return key;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持RSA", e);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("无效的密钥格式");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@SneakyThrows
public static String appPaySign(String prepayid, String nonceStr, String timestamp) {
String signatureStr = Stream.of(WxPayInfoConfig.appid, timestamp, nonceStr, prepayid)
.collect(Collectors.joining("n", "", "n"));
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(WxPayInfoConfig.privateKey);
sign.update(signatureStr.getBytes(StandardCharsets.UTF_8));
return base64Utils.encodeToString(sign.sign());
}
@SneakyThrows
public static boolean verifySign(HttpServletRequest request,String body) {
boolean verify = false;
try {
String wechatPaySignature = request.getHeader("Wechatpay-Signature");
String wechatPayTimestamp = request.getHeader("Wechatpay-Timestamp");
String wechatPayNonce = request.getHeader("Wechatpay-Nonce");
String wechatPaySerial = request.getHeader("Wechatpay-Serial");
//组装签名串
String signStr = Stream.of(wechatPayTimestamp, wechatPayNonce, body)
.collect(Collectors.joining("n", "", "n"));
//获取平台证书
AutoUpdateCertificatesVerifier verifier = getVerifier(wechatPaySerial);
//获取失败 验证失败
if (verifier != null) {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(verifier.getValidCertificate());
//放入签名串
signature.update(signStr.getBytes(StandardCharsets.UTF_8));
verify = signature.verify(base64.getDecoder().decode(wechatPaySignature.getBytes()));
}
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return verify;
}
public static X509Certificate getX509Certificate() {
ClassPathResource resource = new ClassPathResource("wx/apiclient_cert.p12");
KeyStore keyStore;
X509Certificate certificate = null;
try {
keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(resource.getInputStream(), WxPayInfoConfig.mchId.toCharArray());
certificate = (X509Certificate) keyStore.getCertificate("Tenpay Certificate");
certificate.checkValidity();
} catch (KeyStoreException | IOException e) {
e.printStackTrace();
} catch (CertificateNotYetValidException e) {
e.printStackTrace();
} catch (CertificateExpiredException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return certificate;
}
public static String decryptToString(String associatedData, String nonce, String ciphertext) {
String cert = null;
try {
//简化
AesUtil aesUtil = new AesUtil(WxPayInfoConfig.apiV3Key.getBytes(StandardCharsets.UTF_8));
cert = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext);
} catch (GeneralSecurityException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return cert;
}
public static String rsaDecryptOAEP(String ciphertext) {
// 使用商户私钥解密
String text = null;
try {
text = RsaCryptoUtil.decryptOAEP(ciphertext, WxPayInfoConfig.privateKey);
} catch (BadPaddingException e) {
e.printStackTrace();
}
return text;
}
public static String rsaEncryptOAEP(String message) {
String ciphertext = null;
try {
AutoUpdateCertificatesVerifier verifier = getVerifier(null);
X509Certificate certificate = verifier.getValidCertificate();
ciphertext = RsaCryptoUtil.encryptOAEP(message, certificate);
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
}
return ciphertext;
}
public static T wxHttpRequest(String method, String url, String reqdata, Class t) {
T resultBean = null;
T instance = null;
CloseableHttpResponse response;
CloseableHttpClient httpClient = null;
try {
if (!verifierMap.isEmpty()) {
String bigInteger = verifierMap.keys().nextElement();
//httpClient 内置有签名信息
httpClient = getHttpClient(bigInteger);
} else {
httpClient = getHttpClient("1");
}
//创建实体,相当于new对象
instance = t.newInstance();
if ("get".equalsIgnoreCase(method)) {
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("Accept", "application/json");
//完成签名并执行请求
response = httpClient.execute(httpGet);
} else {
StringEntity entity = new StringEntity(reqdata, StandardCharsets.UTF_8);
entity.setContentType("application/json");
HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
response = httpClient.execute(httpPost);
}
//如果状态码为200,就是正常返回
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
String result = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
JSONObject jsonObject = JSON.parseObject(result);
if (instance instanceof JSONObject) {
resultBean = (T) jsonObject;
} else {
resultBean = JSONObject.parseObject(jsonObject.toString(), t);
}
} else if (statusCode == 204) {
log.info("关单成功");
String code = "204";
resultBean = (T) code;
} else {
log.error("错误状态码 = " + statusCode + ",返回的错误信息 = " + EntityUtils.toString(response.getEntity()));
ExceptionCast.cast(CommonCode.WX_ORDER_REAUEST_FAIL);
}
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (httpClient != null) {
httpClient.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return resultBean;
}
public static Map createOrder(Integer totalPrice, String out_trade_no, String
description) {
WxOnePayVo wxonePayVo = new WxOnePayVo(totalPrice);
wxOnePayVo.setAppid(WxPayInfoConfig.appid);
wxOnePayVo.setMchid(WxPayInfoConfig.mchId);
wxOnePayVo.setNotify_url(WxPayInfoConfig.pay_notify_url);
wxOnePayVo.setDescription(description);
wxOnePayVo.setAttach(CommonConstant.pay_order);
wxOnePayVo.setOut_trade_no(out_trade_no);
//计算失效时间 15分钟订单关闭
wxOnePayVo.setTime_expire(CommonUtil.getTimeExpire());
String reqdata = JSON.toJSONString(wxOnePayVo);
Map returnMap = getPayReturnMap("POST", reqdata, WxPayInfoConfig.singleUrl);
return returnMap;
}
public static Map getPayReturnMap(String method, String reqdata, String url) {
HashMap returnMap = new HashMap();
JSONObject jsonObject = wxHttpRequest(method, url, reqdata, JSONObject.class);
//处理成功
String prepay_id = (String) jsonObject.get("prepay_id");
//生成代签名的支付信息
String nonceStr = UUID.randomUUID().toString(true);
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String signature = appPaySign(prepay_id, nonceStr, timestamp);
returnMap.put("appid", WxPayInfoConfig.appid);
returnMap.put("partnerid", WxPayInfoConfig.mchId);
returnMap.put("prepayid", prepay_id);
returnMap.put("package", "Sign=WXPay");
returnMap.put("noncestr", nonceStr);
returnMap.put("timestamp", timestamp);
returnMap.put("sign", signature);
return returnMap;
}
public static WxRefundReturnInfoVo refundOrder(WxRefundInfoVo wxRefundInfoVo) {
wxRefundInfoVo.setNotify_url(WxPayInfoConfig.refund_notify_url);
String reqdata = JSON.toJSONString(wxRefundInfoVo);
WxRefundReturnInfoVo wxRefundReturnInfoVo = wxHttpRequest("POST", WxPayInfoConfig.refundUrl, reqdata, WxRefundReturnInfoVo.class);
return wxRefundReturnInfoVo;
}
public static NotifyResourceVO QueryOrder(String transaction_id) {
String url = String.format(WxPayInfoConfig.queryUrl, transaction_id, WxPayInfoConfig.mchId);
NotifyResourceVO notifyResourceVO = wxHttpRequest("GET", url, null, NotifyResourceVO.class);
return notifyResourceVO;
}
public static String CloseOrder(String out_trade_no) {
String url = String.format(WxPayInfoConfig.closeUrl, out_trade_no);
//请求body参数
String reqdata = "{"mchid": "" + WxPayInfoConfig.mchId + ""}";
String code = wxHttpRequest("POST", url, reqdata, String.class);
return code;
}
}
实体类:
WxOnePayVo:请求预付单标识的关键参数
@Data
public class WxOnePayVo {
private String out_trade_no;
private String mchid;
private String appid;
private String attach;
private String description;
private String notify_url;
private String time_expire;
private Amount amount;
@Data
class Amount {
@ApiModelProperty("总金额")
private Integer total;
@ApiModelProperty("CNY")
private String currency = "CNY";
}
public WxOnePayVo(Integer total){
Amount amount = new Amount();
amount.setTotal(total);
this.amount = amount;
}
}
代码里都有对应的方法,顺序可能有些乱,不过参照上边的流程图看,应该没问题,代码里的加解密敏感信息,我暂时我用到,只用了解密报文(decryptToString)。
第三步:接收异步通知接收异步通知(按照第二步完成支付信息的封装并返回给前端之后,接下来就是默默等待异步通知了,所有支付的后续工作都在这里完成)
流程:定义异步通知接口-接收通知并解析处理
关键代码:
@ApiOperation(value = "微信异步通知", notes = "微信异步通知")
@PostMapping(value = "/wxNotify")
public Map getTenPayNotify(HttpServletRequest request) throws IOException {
HashMap returnMap = new HashMap<>(2);
String body = request.getReader().lines().collect(Collectors.joining());
WxPayAsyncVo wxPayAsyncVo = JSONObject.parseObject(body, WxPayAsyncVo.class);
//验证签名
boolean verifySign = WxPayUtil.verifySign(request, body);
if (verifySign) {
//https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_2_5.shtml
//获得加密数据,得到的资源对象
com.ls.modules.common.vo.wx.pay.Resource resource = wxPayAsyncVo.getResource();
//解密后资源数据
String notifyResourceStr = WxPayUtil.decryptToString(resource.getAssociated_data(), resource.getNonce(), resource.getCiphertext());
//通知资源数据对象
NotifyResourceVO notifyResourceVO = JSONObject.parseObject(notifyResourceStr, NotifyResourceVO.class);
}
}
封装的实体类对象:
WxPayAsyncVo:通知结果大对象
@ToString
@Data
public class WxPayAsyncVo {
private String id;
private String create_time;
private String event_type;
private String resource_type;
private String summary;
private Resource resource;
}
NotifyResourceVO:大对象中包含的资源对象
@Data
public class NotifyResourceVO {
private String appid;
private String mchid;
private String out_trade_no;
private String transaction_id;
private String trade_type;
private String trade_state;
private String trade_state_desc;
private String bank_type;
private String success_time;
private String attach;
private Payer payer;
private NotifyAmount amount;
@Data
public class Payer {
private String openid;
}
}
结束总结:
了解清楚app下单,那么其他的功能,比如:关单、查单、退款等等操作都基本相同,直接在公共类中增加对应的方法即可。
配置证书和各种id,key的时候一定要仔细,最容易出错的就是路径问题,id或者密钥等配反了、错了。
增加新功能的时候多查看微信提供的示例代码,另外注意的是,微信的示例代码有些是V2的或者自己手写实现的,最好看完示例代码,去这里java类库看看,里边封装了很多方法,能够节省很多工作量。



