Java接入微信支付APIV3
1.简介
为了在保证支付安全的前提下,带给商户简单、一致且易用的开发体验,推出了全新的微信支付API v3。
相较于之前的微信支付API,主要区别是:
- 遵循统一的REST 的设计风格
- 使用JSON作为数据交互的格式,不再使用XML
- 使用基于非对称密钥的SHA256-RSA的数字签名算法,不再使用MD5或HMAC-SHA256
- 不再要求HTTPS客户端证书
- 使用AES-256-GCM,对回调中的关键信息进行加密保护
在接口规则中,你将了解到微信支付API v3的基础约定,如数据格式,参数兼容性,错误处理等。随后我们重点介绍了微信支付API v3新的认证机制。你可以跟随着签名指南,使用命令行或者你熟悉的编程语言,一步一步实践如何签名和验签。在最后的常见问题中,我们总结了商户接入过程中的各种常见和不常见的问题。
我们提供了API v3的Postman调试工具和某些开发语言的库。你可以通过我们的Github获取。
如果你有任何问题,欢迎访问我们的开发者社区。
以上是微信支付官方的说明,APIV3版本的微信支付相较于之前版本的不同以及优化之处。
前言:
最近也是新增了一个微信支付的需求,之前写过微信支付的Native支付也就是扫码支付,本次接入的是JSAPI支付/小程序支付,接入的也是最新版的APIV3的版本,在接入过程中也是遇到了不少的坑,新的APIV3的接口文档在很多地方描述的也是不够清晰,也阅读过多篇CSDN大多也都是七零八碎的,特此编写了这篇博客,希望各位小伙伴少踩坑,也让自己不再重复踩坑,如果有描述不当错误之处,恳请各位大佬指正!
微信支付APIV3官方文档
2.JSAPI下单
注: 商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易会话标识后再按Native、JSAPI、APP等不同场景生成交易串调起支付。
想要调用微信支付JSAPI下单,必须先设置HTTP头。微信支付商户API v3要求请求通过HTTP Authorization头来传递签名。
话不多说,贴代码:
1.这是请求JSAPI必须的参数,我写了一个属性类。
/**
* Created by HCH on 2021/12/14.
*/
public class CreateOrderRequestVo {
@ApiModelProperty(value = "appId,公众号名称,由商户传入", required = true)
private String appid;
@ApiModelProperty(value = "直连商户的商户号,由微信支付生成并下发", required = true)
private String mchid;
@ApiModelProperty(value = "商品描述", required = true)
private String description;
@ApiModelProperty(value = "商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一", required = true)
private String out_trade_no;
@ApiModelProperty(value = "异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 公网域名必须为https,如果是走专线接入,使用专线NAT IP或者私有回调域名可使用http", required = true)
private String notify_url;
@ApiModelProperty(value = "订单金额信息", required = true)
private HashMap<String,Object> amount;
@ApiModelProperty(value = "支付者信息", required = true)
private HashMap<String,Object> payer;
}
2.通过调用下面wxDoPostJson(String url, String json)方法,访问JSAPI。
其中两个参数:
url(微信官方JSAPI_url):https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi
json(将属性类CreateOrderRequestVo,转化成json格式的字符串):
贴一个我用来转json字符串格式的代码:
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* javaBean,list,array convert to json string
*
* @param obj javaBean,list,array
* @return json字符串
* @throws Exception 异常
*/
public static String obj2json(Object obj) throws Exception {
return objectMapper.writeValueAsString(obj);
}
3.接下来就是创建Httpclient对象,来访问微信支付_JSAPI下单的请求了
看代码:
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.util.*;
/**
* Created by HCH on 2021/12/14.
*/
/**
* 发送post请求,携带json类型数据
* 如:{"name":"jok","age":"10"}
*
* @param url 请求地址
* @param json json格式参数
* @return
*/
public static String wxDoPostJson(String url, String json) {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
//获取签名请求头
HashMap<String, String> heads = null;
try {
heads = WxSignV3Utils.getSignMap("POST", url, json);
} catch (InvalidKeySpecException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (SignatureException e) {
e.printStackTrace();
}
httpPost.addHeader("Authorization", heads.get("Authorization"));
httpPost.addHeader("Accept", heads.get("Accept"));
httpPost.addHeader("Content-Type",heads.get("Content-Type"));
// 创建请求内容
StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
httpPost.setEntity(entity);
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), getDefaultCharSet());
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
response.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return resultString;
}
其中最关键的是设置HTTP Header 的设置。
微信官方的要求: 请求的签名信息通过HTTP头Authorization 传递。没有携带签名或者签名验证不通过的请求,都不会被执行,并返回401 Unauthorized 。
OK,我们继续,生成签名:
4.生成签名
注意,上面用到了 WxSignV3Utils.getSignMap(“POST”, url, json); 那我先贴出我的 WxSignV3Utils 工具类:
/**
* Created by HCH on 2021/12/14.
*/
@Component
public class WxSignV3Utils {
//V3主商户ID
private static String merchantId;
//微信商户平台APIv3证书序列号
private static String certificateSerialNo;
//私钥(不要把私钥文件暴露在公共场合,如上传到Github,写在客户端代码等。)
//cert_p12_Path
private static String certP12Path;
// 自己配置文件的mchid_路径
@Value("${wx.mchid}")
public void setMerchantId(String merchantId) {
WxSignV3Utils.merchantId = merchantId;
}
@Value("${wx.payment.v3.certificate_Serial_No}")
public void setCertificateSerialNo(String certificateSerialNo) {
WxSignV3Utils.certificateSerialNo = certificateSerialNo;
}
@Value("${wx.payment.v3.cert_p12_Path}")
public void setCertP12Path(String certP12Path) {
WxSignV3Utils.certP12Path = certP12Path;
}
/**
* 使用方法
* @param method 请求方法
* @param url 请求url
* @param body 请求内容
* @return
*/
public static HashMap<String, String> getSignMap(String method, String url, String body) throws InvalidKeySpecException, NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException, SignatureException {
String authorization = getSign(method, url, body);
HashMap<String, String> headsMap = new HashMap<>();
headsMap.put("Authorization", authorization);
headsMap.put("Content-Type", "application/json");
headsMap.put("Accept", "application/json");
return headsMap;
}
public static String getSign(String method, String url, String body) throws NoSuchAlgorithmException, SignatureException, InvalidKeySpecException, InvalidKeyException, UnsupportedEncodingException {
return "WECHATPAY2-SHA256-RSA2048 " + getToken(method, HttpUrl.parse(url), body);
}
public static String getToken(String method, HttpUrl url, String body) throws UnsupportedEncodingException, SignatureException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
String nonceStr = nonceString();
long timestamp = System.currentTimeMillis() / 1000;
String message = buildMessage(method, url, timestamp, nonceStr, body);
String signature = sign(message.getBytes("utf-8"));
return "mchid=\"" + merchantId + "\","
+ "nonce_str=\"" + nonceStr + "\","
+ "timestamp=\"" + timestamp + "\","
+ "serial_no=\"" + certificateSerialNo + "\","
+ "signature=\"" + signature + "\"";
}
public static String sign(byte[] message) throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, SignatureException {
Signature sign = Signature.getInstance("SHA256withRSA");
//sign.initSign(getPKCS8PrivateKey(privateKey));
KeyPair pkcs12 = new KeyPairFactory().createPKCS12(certP12Path,merchantId);
PrivateKey aPrivate = pkcs12.getPrivate();
sign.initSign(aPrivate);
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
}
public 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 String nonceString() {
String currTime = String.format("%d", (long) System.currentTimeMillis() / 1000);
String strTime = currTime.substring(8, currTime.length());
Random random = new Random();
int num = (int) (random.nextDouble() * (1000000 - 100000) + 100000);
String code = String.format("%06d", num);
String nonce_str = currTime.substring(2) + code;
return nonce_str;
}
}
/**
* Created by HCH on 2021/12/14.
*/
public class KeyPairFactory {
private KeyStore store;
private final Object lock = new Object();
/**
* 获取公私钥.
*
* @param keyPath the key path
* @param keyPass password
* @return the key pair
*/
public KeyPair createPKCS12(String keyPath,String keyPass) {
ClassPathResource resource = new ClassPathResource(keyPath);
char[] pem = keyPass.toCharArray();
try {
synchronized (lock) {
if (store == null) {
synchronized (lock) {
store = KeyStore.getInstance("PKCS12");
store.load(resource.getInputStream(), pem);
}
}
}
X509Certificate certificate = (X509Certificate) store.getCertificate("Tenpay Certificate");
certificate.checkValidity();
// 证书的序列号
String serialNumber = certificate.getSerialNumber().toString(16).toUpperCase();
// 证书的公钥
PublicKey publicKey = certificate.getPublicKey();
// 证书的私钥
PrivateKey storeKey = (PrivateKey) store.getKey("Tenpay Certificate", pem);
return new KeyPair(publicKey, storeKey);
} catch (Exception e) {
throw new IllegalStateException("Cannot load keys from store: " + resource, e);
}
}
注意里面有两个属性,都是在商户申请微信支付成功以后会给你的。
获取步骤:登录微信支付商户平台,进入【账户中心】->【账户设置】->【API安全】
certificateSerialNo:微信商户平台APIv3证书序列号。
certP12Path:证书 apiclient_cert.p12 的路径。
什么是商户API证书?如何获取商户API证书?
当我们配置好这些需要的参数,和证书的时候就可以访问,微信支付_JSAPI下单了。
还是贴一下postman的接口测试格式吧
4.生成JSAPI调起支付API参数
通过JSAPI下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的前端JS方法调起公众号支付。
接口说明
适用对象: 直连商户
接口定义
此API无后台接口交互,需要将列表中的数据签名
- 但是我们后台接口需要生成 前端调起 所需要的 接口参数。
OK,话不多说,贴代码:
import com.alibaba.fastjson.JSONObject;
// 调用方法
JSONObject paySign = WxSignV3Utils.WxTuneUp(prepayId, appid);
可以看到上面调用了 WxSignV3Utils 的 WxTuneUp() 方法。OK,继续贴代码,注意这是WxSignV3Utils类里面的方法:
/**
* 微信调起支付参数
* 返回参数如有不理解 请访问微信官方文档
* https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_1_4.shtml
*
* @param prepayId 微信下单返回的prepay_id
* @param appId 应用ID(appid)
* @return 当前调起支付所需的参数
* @throws Exception
*/
public static JSONObject WxTuneUp(String prepayId, String appId) 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());
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 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();
}
OK,然后将生成的参数返回给前端就OK了!