我自己琢磨出来的,只不过我用的是微信wechatpay-apache-httpclient 0.4.4版本 1.常见术语说明 - 证书:也就是常规意义上的rsa证书,包括公钥和私钥,一般敏感信息(姓名、身份证、手机)等都是通过rsa进行加密解密 - 秘钥:在微信官方文档叫做秘钥,在微信官方sdk里面叫做aesKey或apiKey,其实就是用于aes解密的密钥,一般的AEAD_AES_256_GCM都是通过aes加密解密 - 商户API证书:商户从[微信支付](https://pay.weixin.qq.com/)申请的证书,在【账户中心】->【API安全】->【申请API证书】菜单申请,申请的是rsa证书(一般会有3个文件,第一个p12后缀文件,这个一般用不到,另外两个pem后缀的文件,是商户的rsa公钥和私钥),一般商户证书有5年有效期 - 商户API V3秘钥:注意不是上面商户API证书的私钥,而是商户在【账户中心】->【API安全】->【设置APIv3密钥 】菜单设置的秘钥,这个是32 byte的字符串(ascii字符就行,不建议使用特殊字符) - 平台证书:在微信官方文档也叫做“微信支付平台证书”,也就是微信官方使用的rsa公钥(私钥是微信官方持有),这个公钥比较特殊,需要我们使用接口下载,而且这个rsa公钥可能会到期,需要我们定期下载,目前微信官方的(wechatpay-apache-httpclient v0.4.4)内置了定期更新微信支付平台证书的功能,所以我们一般不需要考虑微信支付平台证书过期的问题 - 签名:微信支付的api在请求或者响应里面一般都需要进行签名,目前微信官方的(wechatpay-apache-httpclient v0.4.4)自动集成了签名的功能,我们直接使用即可 2.快速接入开发 2.1 商户配置类 注意下方CertificatesManager如果需要注册多个商户号,则需要使用putMerchant方法,下面因为只有一个商户号,所以直接在里面putMerchant,如果有多个商户号,则需要针对每个不同商户号的证书、密钥做对应处理 @Component @Slf4j public class WechatPayApiV3Config { /** * 商户号(我们自己的商户号,在微信支付服务商平台里面申请的商户号) */ @Value("${wechat.merchant.id}") private String mchId; /** * apiv3证书密钥,(微信支付平台 -【账户中心】-【api安全】-【设置APIv3密钥 】这里设置的密钥) */ @Value("${wechat.merchant.api.v3.key}") private String apiV3Key; /** * 商户api证书序列号 (微信支付平台 -【账户中心】-【api安全】-【申请API证书】这里申请的证书序列号) */ @Value("${wechat.merchant.certificate.serial}") private String mchSerialNo; /** * 商户rsa私钥文本,就是微信支付平台 -【账户中心】-【api安全】-【申请API证书】最终下载下来的rsa证书私钥文件的内容 */ private static String privateKey; /** * 商户rsa私钥文本转成PrivateKey对象 */ public static PrivateKey merchantPrivateKey; static { try (InputStream in = WechatPayApiV3Config.class.getClassLoader().getResourceAsStream("wechatpayapiv3/apiclient_key.pem")) { byte[] bytes = in.readAllBytes(); privateKey = new String(bytes, StandardCharsets.UTF_8); merchantPrivateKey = PemUtil.loadPrivateKey(privateKey); } catch (IOException e) { log.error("load api client key error: {}", ExceptionUtil.getMessage(e)); } } /** * CertificatesManager利用了ConcurrentHashMap存储多个商户的证书和密钥信息,所以支持多个商户号使用 * * @return * @throws GeneralSecurityException * @throws IOException * @throws HttpCodeException */ @Bean(destroyMethod = "stop") public CertificatesManager certificatesManager() throws GeneralSecurityException, IOException, HttpCodeException { CertificatesManager certificatesManager = CertificatesManager.getInstance(); certificatesManager.putMerchant(mchId, new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)), apiV3Key.getBytes(StandardCharsets.UTF_8)); return certificatesManager; } /** * Verifier是通过CertificatesManager导出的,是严格关联到某一个商户号的,所以不同的商户号需要对应不同的Verifier,如果生产环境有多个商户号, * 则建议将不同商户号的Verifier缓存到Map里面,因为certificatesManager.getVerifier(mchId);每次都会创建一个新的Verifier对象,频繁创建对象不好 * * @param certificatesManager * @return * @throws NotFoundException */ @Bean public Verifier verifier(CertificatesManager certificatesManager) throws NotFoundException { return certificatesManager.getVerifier(mchId); } /** * CloseableHttpClient是微信支付的client,内置了http请求的签名以及签名解密验证功能,所以说我们一般无需手工处理签名的加密、解密、验证, * 而且CloseableHttpClient也是严格关联到某一个商户号的,所以不同的商户号需要对应不同的CloseableHttpClient,如果生产环境有多个商户号, * 则建议将不同商户号的CloseableHttpClient缓存到Map里面 * * @param verifier * @return */ @Bean public CloseableHttpClient wechatPayApiV3HttpClient(Verifier verifier) { return WechatPayHttpClientBuilder.create() .withMerchant(mchId, mchSerialNo, merchantPrivateKey) .withValidator(new WechatPay2Validator(verifier)) .build(); } } 2.2 手工生成signature签名 & 手工验证签名 我们发送请求给微信,都是需要对请求参数、url等进行签名,前面提到了微信的CloseableHttpClient内置了请求自动签名,响应签名解密验证,如果某些特殊情况下需要手工验证签名(例如:微信回调我们的服务) 注意:微信支付的签名都是使用rsa加密解密的。 2.2.1 商户侧生成签名 商户侧生成签名是需要使用商户自己的私钥进行加密,详情可以参考(wechatpay-apache-httpclient v0.4.4)WechatPay2Credentials的getToken方法,在这个方法中会使用到PrivateKeySigner对请求信息进行签名,一般通过微信的CloseableHttpClient发送的请求,内部自动做好了请求签名、响应签名验证,所以无需我们手工操作签名 2.2.2 商户侧验证签名 只有那些微信主动回调我们接口的地方,这时候就需要我们主动验证签名的有效性,防止黑客伪装签名数据,微信的响应或者回调,都会在http头部增加这么几个属性Wechatpay-Serial、Wechatpay-Signature、Wechatpay-Timestamp、Wechatpay-Nonce,我们拿到请求或响应body内容,接着通过verify进行验证签名 /** * 验证签名 * * @param body * @param nonce * @param serial * @param timestamp * @param signature * @return */ public boolean verify(String body, String nonce, String serial, String timestamp, String signature) { NotificationRequest request = new NotificationRequest.Builder().withSerialNumber(serial) .withNonce(nonce) .withTimestamp(timestamp) .withSignature(signature) .withBody(body) .build(); return verifier.verify(request.getSerialNumber(), request.getMessage(), request.getSignature()); } 2.3 数据加密解密 2.3.3 AEAD_AES_256_GCM解密 /** * aes解密 * * @param nonce * @param associatedData * @param ciphertext * @return */ public String aesDecrypt(String nonce, String associatedData, String ciphertext) throws GeneralSecurityException { AesUtil aesUtil = new AesUtil(apiV3Key.getBytes(UTF_8)); return aesUtil.decryptToString(associatedData.getBytes(UTF_8), nonce.getBytes(UTF_8), ciphertext); } 2.3.3 敏感信息加密 发送给微信的敏感信息,需要使用微信支付平台证书(rsa公钥)进行加密,加密之后,还需要在请求头增加一个Wechatpay-Serial的头部,这个头部就是微信支付平台证书的序列号(如何拿到序列号参考下列代码) //微信支付平台证书(rsa公钥),在CertificatesManager底层会定期更新的 X509Certificate certificate = verifier.getValidCertificate(); //拿到证书序列号(10进制的内容) BigInteger serialNumber = certificate.getSerialNumber(); //转成16进制字符串 String wechatPaySerial = serialNumber.toString(16).toUpperCase(); //发送给微信的敏感信息加密 String encrypt = RsaCryptoUtil.encryptOAEP("明文内容", certificate); HttpPost post = new HttpPost(url); String requestBody = JSONObject.toJSONString(request); StringEntity entity = new StringEntity(requestBody, APPLICATION_JSON); post.setEntity(entity); //设置请求头部Wechatpay-Serial,注意这里是微信支付平台证书序列号,不是商户证书序列号,如果用了商户证书序列号则微信会返回{"code":"PARAM_ERROR","message":"平台证书序列号Wechatpay-Serial错误"} post.addHeader(WechatPayHttpHeaders.WECHAT_PAY_SERIAL, wechatPaySerial); //设置请求头部ACCEPT,需要设置accept请求头,否则微信api会报错 {"code":"INVALID_REQUEST","message":"头部信息不完整"} post.addHeader(ACCEPT, APPLICATION_JSON.toString()); CloseableHttpResponse response = wechatPayApiV3HttpClient.execute(post); int statusCode = response.getStatusLine().getStatusCode(); byte[] bytes = response.getEntity().getContent().readAllBytes(); String content = new String(bytes, UTF_8); 2.3.4 敏感信息解密 微信返回响应或者回调我们接口的敏感信息,微信使用了商户的公钥进行加密,所以我们自己需要用商户私钥解密 //解密密文信息 String message = RsaCryptoUtil.decryptOAEP("密文信息", WechatPayApiV3Config.merchantPrivateKey)
v3接口规则的回调验签怎么知道使用的哪个平台证书?看官方给的范例 回调通知是这个验签使用的verifier怎么获取呢?回调是全是密文的 如果要通过获取平台证书接口 是需要商户号的呀!![图片]
2022-04-22