微信支付分,APIv3版本接口对接过程(附代码)

刚对接完微信支付分,对接过程还是有点小坑,微信官方的接口文档写的比较粗略,代码示例比较少,网上的相关技术博客少之又少,前期还是有点小困难的,所以决定把对接过程梳理一下,希望能帮到需要的人。

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

你可能感兴趣的:(微信支付分,APIv3版本接口对接过程(附代码))