实战微信支付 APIv3 接口

Posted sp42a

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实战微信支付 APIv3 接口相关的知识,希望对你有一定的参考价值。

开场白直接引用官方文档的吧。

为了在保证支付安全的前提下,带给商户简单、一致且易用的开发体验,我们推出了全新的微信支付 APIv3 接口。

为啥不用官方 SDK?

官方 SDK 不错,只是依赖 Apache-httpclient,可是我连 Apache-httpclient 都不想用啊,于是就自行接入。其实官方文档也很详尽,只是有点乱(否则就没有我写本文的需要啦)。官方文档如是说。

在规则说明中,你将了解到微信支付API v3的基础约定,如数据格式、参数兼容性、错误处理、UA说明等。我们还重点介绍了微信支付API v3新的认证机制(证书/密钥/签名)。你可以跟随着开发指南,使用命令行或者你熟悉的编程语言,一步一步实践签名生成、签名验证、证书和回调报文解密和敏感信息加解密。在最后的常见问题中,我们总结了商户接入过程遇到的各种问题。

准备条件

该申请的都申请,把所需的条件准备好。形成如下 Java POJO 要求的字段。

/**
 * 微信支付 商户配置
 * 
 * @author Frank Cheung<sp42@qq.com>
 *
 */
public class MerchantConfig 
	/**
	 * 商户号
	 */
	private String mchId;

	/**
	 * 商户证书序列号
	 */
	private String mchSerialNo;

	/**
	 * V3 密钥
	 */
	private String apiV3Key;

	/**
	 * 商户私钥
	 */
	private String privateKey;

	public String getMchId() 
		return mchId;
	

	public void setMchId(String mchId) 
		this.mchId = mchId;
	

	public String getMchSerialNo() 
		return mchSerialNo;
	

	public void setMchSerialNo(String mchSerialNo) 
		this.mchSerialNo = mchSerialNo;
	

	public String getApiV3Key() 
		return apiV3Key;
	

	public void setApiV3Key(String apiV3Key) 
		this.apiV3Key = apiV3Key;
	

	public String getPrivateKey() 
		return privateKey;
	

	public void setPrivateKey(String privateKey) 
		this.privateKey = privateKey;
	


签名

访问商户平台的支付接口都要在 HTTP Head 加上签名才能访问。下图以小程序的为例子。


如何生成签名?下面按照文档指引,以获取商户平台证书为例子,生成签名。

准备私钥

首先你要准备好商户 API 证书里面的私钥(Private Key),例如我当前读取磁盘的证书(当然这个到时要部署到服务器资源目录下)。

private String privateKey = FileHelper.openAsText("C:\\\\Users\\\\frank\\\\Downloads\\\\WXCertUtil\\\\cert\\\\1623777099_20220330_cert\\\\apiclient_key.pem");

@Autowired
private MerchantConfig cfg;
……
cfg.setPrivateKey(privateKey);// 保存到配置

转换为 Java 里面的 PrivateKey 对象,依靠下面的工具类 PemUtil

public class PemUtil 
	public static PrivateKey loadPrivateKey(String privateKey) 
		privateKey = privateKey.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replaceAll("\\\\s+", "");

		try 
			KeyFactory kf = KeyFactory.getInstance("RSA");

			return kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
		 catch (NoSuchAlgorithmException e) 
			throw new RuntimeException("当前Java环境不支持RSA", e);
		 catch (InvalidKeySpecException e) 
			throw new RuntimeException("无效的密钥格式");
		
	
	……

签名生成器

签名过程参见文档,此处不再赘述。除了一般的时间戳、请求随机串(nonce_str)等等之外,签名要求内容有请求接口的 HTTP 方法、URL 和 请求报文主体,为此我们准备一个简单的 Bean。

/**
 * 请求接口的 HTTP 方法、URL 和 请求报文主体
 * 
 * @author Frank Cheung<sp42@qq.com>
 *
 */
public class HttpRequestWrapper 
	public String method;
	public String url;
	public String body;

我们看看调用例子。

HttpRequestWrapper r = new HttpRequestWrapper();
r.method = "GET";
r.url = "/v3/certificates";
r.body = "";

SignerMaker signer = new SignerMaker(cfg);
String token = signer.getToken(r);// 得到签名

签名生成器 SignerMaker 源码如下。

import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;

import com.ajaxjs.util.StrUtil;
import com.ajaxjs.util.logger.LogHelper;
import com.ajaxjs.wechat.applet.util.PemUtil;
import com.ajaxjs.wechat.applet.util.RsaCryptoUtil;

/**
 * 签名生成器
 * 
 * @author Frank Cheung<sp42@qq.com>
 *
 */
public class SignerMaker 
	private static final LogHelper LOGGER = LogHelper.getLog(SignerMaker.class);

	private MerchantConfig cfg;

	protected final PrivateKey privateKey;

	/**
	 * 创建签名生成器
	 * 
	 * @param cfg 商户平台的配置
	 */
	public SignerMaker(MerchantConfig cfg) 
		this.cfg = cfg;
		this.privateKey = PemUtil.loadPrivateKey(cfg.getPrivateKey());
	

	/**
	 * 生成签名
	 * 
	 * @param request
	 * @return 签名 Token
	 */
	public String getToken(HttpRequestWrapper request) 
		String nonceStr = StrUtil.getRandomString(32);
		long timestamp = System.currentTimeMillis() / 1000;

		String message = buildMessage(request, nonceStr, timestamp);
		LOGGER.debug("authorization message=[0]", message);

		String signature = RsaCryptoUtil.sign(privateKey, message.getBytes(StandardCharsets.UTF_8));

		// @formatter:off
        String token = "mchid=\\"" + cfg.getMchId() + "\\","
                + "nonce_str=\\"" + nonceStr + "\\","
                + "timestamp=\\"" + timestamp + "\\","
                + "serial_no=\\"" + cfg.getMchSerialNo() + "\\","
                + "signature=\\"" + signature + "\\"";
        // @formatter:on

		LOGGER.debug("authorization token=[0]", token);

		return token;
	

	/**
	 * 
	 * @param request
	 * @param nonceStr
	 * @param timestamp
	 * @return
	 */
	static String buildMessage(HttpRequestWrapper request, String nonceStr, long timestamp) 
		// @formatter:off
	    return request.method + "\\n"
	        + request.url + "\\n"
	        + timestamp + "\\n"
	        + nonceStr + "\\n"
	        + request.body + "\\n";
	    // @formatter:on
	

从代码量看确实比以前简单了。

对签名数据进行签名

上述 getToken() 里面会调用 RsaCryptoUtil.sign(),其源码如下。

/**
 * 对签名数据进行签名。
 * 
 * 使用商户私钥对待签名串进行 SHA256 with RSA 签名,并对签名结果进行 Base64 编码得到签名值。
 * 
 * @param message
 * @return 签名结果
 */
public static String sign(PrivateKey privateKey, byte[] message) 
	try 
		Signature sign = Signature.getInstance("SHA256withRSA");
		sign.initSign(privateKey);
		sign.update(message);

		return StrUtil.base64Encode(sign.sign());
	 catch (NoSuchAlgorithmException e) 
		throw new RuntimeException("当前 Java 环境不支持 SHA256withRSA", e);
	 catch (SignatureException e) 
		throw new RuntimeException("签名计算失败", e);
	 catch (InvalidKeyException e) 
		throw new RuntimeException("无效的私钥", e);
	

测试

得到签名 Token 后就可以放在请求头里面测试了,如下获取证书,这是我自己封装的请求方法(Get.api())。

商户API证书 v.s 微信支付平台证书

事情复杂起来了,

获取平台证书

参见文档更新指引

以上是关于实战微信支付 APIv3 接口的主要内容,如果未能解决你的问题,请参考以下文章

微信支付APIv3

微信支付APIv3版,平台证书可视化下载工具

微信支付apiv3注意事项

微信支付apiv3注意事项

微信支付apiv3注意事项

.Net微信服务商平台ApiV3接口