刚对接完微信支付分,对接过程还是有点小坑,微信官方的接口文档写的比较粗略,代码示例比较少,网上的相关技术博客少之又少,前期还是有点小困难的,所以决定把对接过程梳理一下,希望能帮到需要的人。
APIv3版本的接口和之前的API接口有几个重要的区别:
1.使用JSON作为数据交互的格式,不再使用XML
2.使用基于非对称密钥的SHA256-RSA的数字签名算法,不再使用MD5或HMAC-SHA256
3.使用AES-256-GCM,对回调中的关键信息进行加密保护
一、请求签名
所有请求微信方的接口都要使用SHA256-RSA签名算法,生成签名。签名需要用到商户的API证书,在获取商户的API证书之前需要对API证书进行升级。签名过程还会用到商户API证书序列号serial_no和商户号。构造签名串的过程描述可以查看微信文档,生成签名。
生成签名代码如下:
//method(请求类型GET、POST url(请求url) body(请求body,GET请求时body传"",POST请求时body为请求参数的json串) merchantId(商户号) certSerialNo(API证书序列号) keyPath(API证书路径)
public static String getToken(String method,String url, String body,String merchantId,String certSerialNo,String keyPath) throws Exception {
String signStr = "";
HttpUrl httpurl = HttpUrl.parse(url);
String nonceStr = getNonceStr();
long timestamp = System.currentTimeMillis() / 1000;
if(StringUtils.isEmpty(body)){
body = "";
}
String message = buildMessage(method, httpurl, timestamp, nonceStr, body);
String signature = sign(message.getBytes("utf-8"),keyPath);
signStr = "mchid=\"" + merchantId
+ "\",nonce_str=\"" + nonceStr
+ "\",timestamp=\"" + timestamp
+ "\",serial_no=\"" + certSerialNo
+ "\",signature=\"" + signature + "\"";
logger.info("Authorization Token:" +signStr);
return signStr;
}
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 sign(byte[] message,String keyPath) throws Exception {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(getPrivateKey(keyPath));
sign.update(message);
return Base64.encodeBase64String(sign.sign());
}
/**
* 获取私钥。
*
* @param filename 私钥文件路径 (required)
* @return 私钥对象
*/
public static PrivateKey getPrivateKey(String filename) throws IOException {
String content = new String(Files.readAllBytes(Paths.get(filename)), "utf-8");
logger.info("File content:"+content);
try {
String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
logger.info("privateKey:"+privateKey);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(
new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey)));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持RSA", e);
} catch (InvalidKeySpecException e) {
logger.info("异常:"+e);
throw new RuntimeException("无效的密钥格式");
}
}
签名串之后,要设置请求头,GET、POST设置都一样,"Content-Type", "application/json","Accept", "application/json",设置成其他会报错。
httpPost.addHeader("Content-Type", "application/json");
httpPost.addHeader("Accept", "application/json");
httpPost.setHeader("Authorization", "WECHATPAY2-SHA256-RSA2048"+" "+getToken("POST",url,body,merchantId,certSerialNo,keyPath));
生成签名结束
二、签名验证
由于微信支付API v3使用微信支付的平台私钥(不是商户私钥)进行应答签名。所以我们应使用微信支付平台证书中的公钥验签。首先我们要获取平台证书列表,说明可参看微信官方文档,官方文档比较粗略,只展示了请求和应答示例,没有代码demo,具体的获取如下:
public static List getCertByAPI(String merchantId,String url,int timeout,String body,String certSerialNo,String keyPath) throws UnsupportedEncodingException, Exception{
String result = "";
//创建http请求
HttpGet httpGet = new HttpGet(url);
httpGet.addHeader("Content-Type", "application/json");
httpGet.addHeader("Accept", "application/json");
//设置认证信息
httpGet.setHeader("Authorization", "WECHATPAY2-SHA256-RSA2048"+" "+getToken("GET",url,null,merchantId,certSerialNo,keyPath));
//设置请求器配置:如超时限制等
RequestConfig config = RequestConfig.custom().setSocketTimeout(timeout * 1000).setConnectTimeout(timeout * 1000).build();
httpGet.setConfig(config);
List x509Certs = new ArrayList();
try {
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = httpClient.execute(httpGet);
int statusCode = response.getStatusLine().getStatusCode();
HttpEntity httpEntity = response.getEntity();
result = EntityUtils.toString(httpEntity, "UTF-8");
if(statusCode == 200){
logger.info("下载平台证书返回结果:"+result);
List certList = new ArrayList();
JSONObject json = JSONObject.parseObject(result);
logger.info("查询结果json字符串转证书List:"+json.get("data"));
JSONArray jsonArray = (JSONArray)json.get("data");
for(int i=0;i plainList = decrypt(certList,response);
if(CollectionUtils.isNotEmpty(plainList)){
logger.info("平台证书开始保存");
x509Certs = saveCertificate(plainList);
}
}
response.close();
httpClient.close(); //throw
return x509Certs;
} catch (Exception e) {
e.printStackTrace();
logger.error("下载平台证书返回结果:"+e);
}
return x509Certs;
}
//平台证书item
public class CertificateItem {
//加密的平台证书序列号
private String serial_no;
//加密的平台证书序列号
private String effective_time;
//证书弃用时间
private String expire_time;
//证书加密信息
private EncryptedCertificateItem encrypt_certificate;
}
//证书明文item
public class PlainCertificateItem {
private String serialNo;
private String effectiveTime;
private String expireTime;
private String plainCertificate;
}
//证书保存
private static List saveCertificate(List cert) throws IOException {
delFiles();
List x509Certs = new ArrayList();
File file = new File("平台证书路径");
file.mkdirs();
for (PlainCertificateItem item : cert) {
ByteArrayInputStream inputStream = new ByteArrayInputStream(item.getPlainCertificate().getBytes(StandardCharsets.UTF_8));
X509Certificate x509Cert = PemUtil.loadCertificate(inputStream);
x509Certs.add(x509Cert);
String outputAbsoluteFilename = file.getAbsolutePath() + File.separator + "wechatpay_" + item.getSerialNo() + ".pem";
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputAbsoluteFilename), StandardCharsets.UTF_8))) {
writer.write(item.getPlainCertificate());
}
logger.info("输出证书文件目录:"+outputAbsoluteFilename);
}
return x509Certs;
}
private static List decrypt(List certList,CloseableHttpResponse response) throws GeneralSecurityException, IOException{
List plainCertificateList = new ArrayList();
AesUtil aesUtil = new AesUtil("APIv3密钥").getBytes(StandardCharsets.UTF_8));
for(CertificateItem item:certList){
PlainCertificateItem bo = new PlainCertificateItem();
bo.setSerialNo(item.getSerial_no());
bo.setEffectiveTime(item.getEffective_time());
bo.setExpireTime(item.getExpire_time());
logger.info("平台证书密文解密");
bo.setPlainCertificate(aesUtil.decryptToString(item.getEncrypt_certificate().getAssociated_data().getBytes(StandardCharsets.UTF_8),
item.getEncrypt_certificate().getNonce().getBytes(StandardCharsets.UTF_8), item.getEncrypt_certificate().getCiphertext()));
logger.info("平台证书公钥明文:"+bo.getPlainCertificate());
plainCertificateList.add(bo);
}
return plainCertificateList;
}
验证签名文档参考,微信官方文档
通知回调的证书验证,证书解密以及报文解密可以反编译第三方工具包查看,Java证书下载工具 - CertificateDownloader