Apache License 2.0

技术解读

以下内容由 GitRead 中文社区 编辑团队基于项目 README 与官方资料整理, 结合中文开发者的常见使用场景补充技术选型建议。具体以项目官方文档为准。

微信支付 APIv3 Apache HttpClient 装饰器深度技术分析

项目背景与痛点

微信支付作为国内最广泛使用的线上支付方式之一,其 API 接口一直以来都备受开发者关注。然而,在实际开发中,开发者们往往面临线程安全、请求重试策略和复杂的参数配置等挑战。微信支付 APIv3 官方在提供高效的客户端时,未能完全覆盖这些痛点。

“wechatpay-apiv3/wechatpay-apache-httpclient”项目通过使用 Apache HttpClient,为开发者提供了一个高度可配置、线程安全以及具备重试机制的客户端库。它主要解决了以下核心问题:

  • 线程安全性:在多线程环境下保证 API 调用的线程安全。
  • 灵活的重试策略:在出现网络异常时自动重试,提高 API 调用的成功率。
  • 参数管理:简化了参数的配置过程。

核心技术揭秘

微信支付 APIv3 客户端在技术架构中采用了一系列设计模式与技术栈,保证其高效性与可维护性。下面我们通过一张 Mermaid 图来解析这一过程。

graph TD A[用户发起请求] --> B{"初始化 HttpClient"} B -->|"成功"| C[封装请求] B -->|"失败"| D[返回初始化错误] C --> E[参数校验] E -->|"通过"| F{"发送请求"} F -->|"2xx状态码"| G[处理响应] F -->|"非2xx状态码"| H{"重试机制"} H -->|"仍未成功"| I[记录日志] H -->|"成功"| G G --> J[返回结果]

关键技术栈与设计模式

  1. 线程安全性:通过引用 org.apache.http.impl.client.HttpClients 创建线程安全的 CloseableHttpClient 实例。
  2. 重试机制:使用 IOExceptionRetryStrategy 接口并实现自定义策略。当遇到诸如 ConnectTimeoutException 等网络异常时,自动重试。
  3. 配置管理:引入 DefaultHttpClientBuilder,通过设置 HttpClient 相关参数简化用户配置。

数据流

  • 请求进入:用户调用客户端接口执行支付操作。
  • 初始化:客户端初始化 HttpClient 实例,确保线程安全。
  • 参数封装:将用户调用参数封装为 HTTP 请求体。
  • 参数校验:验证请求参数的正确性。
  • 发送请求:利用 execute(HttpUriRequest) 方法发送请求。
  • 处理响应:根据响应状态码,继续处理或重试。
  • 返回结果:将最终结果返回给用户。

功能亮点与差异

该装饰器实现了如下关键功能,提供了区别于竞品的优势:

  • 自动重试机制:显著提高了 API 调用的成功率,提供稳定的支付体验。
  • 线程安全:为多线程环境下的应用提供了可靠的解决方案。
  • 自定义配置:允许开发者灵活定制重试次数等关键参数。

相较于同类工具,它专注解耦多线程环境下的支付请求处理,提供了一系列丰富的功能,帮助开发者快速响应业务需求。

应用场景与落地建议

在生产环境中使用此装饰器时,需注意以下几点:

  • 检查线程安全性:确保应用在多线程环境中能正确初始化与利用 HttpClient 进行调用。
  • 配置重试策略:根据实际业务场景,合理调整重试策略的参数,避免过多重试造成资源浪费。
  • 监控异常:在引入重试机制的同时,引入监控和日志记录功能,及时记录并处理请求重试失败情况。

此装饰器非常适合用于需要处理多线程支付请求、要求稳定性和高成功率的应用场景。

综合评价

优点:

  • 稳定性强:通过高效的重试机制确保了更高的请求成功率。
  • 易于配置:提供了线程安全的 HttpClient 实例,且具有丰富的配置选项。
  • 灵活性高:用户可根据实际需要自定义各种配置参数,满足不同业务需求。

缺点:

  • 依赖包较多:引入了 Apache HttpClient,可能会带来较大的依赖开销。
  • 使用门槛:对于不熟悉 HttpClient 使用的开发者来说,可能存在一定的学习成本。

总的来看,“wechatpay-apiv3/wechatpay-apache-httpclient”项目在处理微信支付 API 调用时提供了稳定可靠的方案,为多线程环境下的开发者提供强大支持,是一份值得推荐的应用实例。

简要分析

热度分
1240
价值分
509
活跃状态
活跃

语言占比

Java

Release

README

wechatpay-apache-httpclient

概览

微信支付API v3的Apache HttpClient扩展,实现了请求签名的生成和应答签名的验证。

Important 重要

我们强烈建议你改为使用 WechatPay-Java,该SDK同样支持 Apache HttpClient 且提供了更完善的功能,本库未来只会进行必要的修复更新。

项目状态

当前版本0.6.0为测试版本。请商户的专业技术人员在使用时注意系统和软件的正确性和兼容性,以及带来的风险。

升级指引

若你使用的版本为<=0.5.0,升级前请参考升级指南。

环境要求

  • Java 1.8+

安装

最新版本已经在 Maven Central 发布。

Gradle

在你的build.gradle文件中加入如下的依赖

implementation 'com.github.wechatpay-apiv3:wechatpay-apache-httpclient:0.6.0'

Maven

加入以下依赖

<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-apache-httpclient</artifactId>
    <version>0.6.0</version>
</dependency>

名词解释

  • 商户API证书,是用来证实商户身份的。证书中包含商户号、证书序列号、证书有效期等信息,由证书授权机构(Certificate Authority ,简称CA)签发,以防证书被伪造或篡改。如何获取请见商户API证书。
  • 商户API私钥。商户申请商户API证书时,会生成商户私钥,并保存在本地证书文件夹的文件apiclient_key.pem中。注:不要把私钥文件暴露在公共场合,如上传到Github,写在客户端代码等。
  • 微信支付平台证书。平台证书是指由微信支付负责申请的,包含微信支付平台标识、公钥信息的证书。商户可以使用平台证书中的公钥进行应答签名的验证。获取平台证书需通过获取平台证书列表接口下载。
  • 微信支付公钥。由微信支付生成,商户可以使用该公钥进行应答签名、回调签名的验证,详见:微信支付公钥。
  • 证书序列号。每个证书都有一个由CA颁发的唯一编号,即证书序列号。如何查看证书序列号请看这里。
  • API v3密钥。为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了AES-256-GCM加密。API v3密钥是加密时使用的对称密钥。商户可以在【商户平台】->【API安全】的页面设置该密钥。

开始

如果你使用的是HttpClientBuilder或者HttpClients#custom()来构造HttpClient,你可以直接替换为WechatPayHttpClientBuilder。

import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;

//...
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
        .withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
        .withWechatPay(wechatpayPublicKeyId, wechatPayPublicKey);
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient

// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签
CloseableHttpClient httpClient = builder.build();

// 后面跟使用Apache HttpClient一样
CloseableHttpResponse response = httpClient.execute(...);

参数说明:

  • merchantId商户号。
  • merchantSerialNumber商户API证书的证书序列号。
  • merchantPrivateKey商户API私钥,如何加载商户API私钥请看常见问题。
  • wechatpayPublicKeyId微信支付公钥ID,登录商户平台可获取,详见:获取微信支付公钥
  • wechatPayPublicKey微信支付公钥,登录商户平台可获取,详见:获取微信支付公钥

示例:获取平台证书

你可以使用WechatPayHttpClientBuilder构造的HttpClient发送请求和应答了。

URIBuilder uriBuilder = new URIBuilder("https://api.mch.weixin.qq.com/v3/certificates");
HttpGet httpGet = new HttpGet(uriBuilder.build());
httpGet.addHeader("Accept", "application/json");

CloseableHttpResponse response = httpClient.execute(httpGet);

String bodyAsString = EntityUtils.toString(response.getEntity());
System.out.println(bodyAsString);

示例:JSAPI下单

注:

  • 我们使用了 jackson-databind 演示拼装 Json,你也可以使用自己熟悉的 Json 库
  • 请使用你自己的测试商户号、appid 以及对应的 openid
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type","application/json; charset=utf-8");

ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectMapper objectMapper = new ObjectMapper();

ObjectNode rootNode = objectMapper.createObjectNode();
rootNode.put("mchid","1900009191")
        .put("appid", "wxd678efh567hg6787")
        .put("description", "Image形象店-深圳腾大-QQ公仔")
        .put("notify_url", "https://www.weixin.qq.com/wxpay/pay.php")
        .put("out_trade_no", "1217752501201407033233368018");
rootNode.putObject("amount")
        .put("total", 1);
rootNode.putObject("payer")
        .put("openid", "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o");

objectMapper.writeValue(bos, rootNode);

httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
CloseableHttpResponse response = httpClient.execute(httpPost);

String bodyAsString = EntityUtils.toString(response.getEntity());
System.out.println(bodyAsString);

示例:查单

URIBuilder uriBuilder = new URIBuilder("https://api.mch.weixin.qq.com/v3/pay/transactions/id/4200000889202103303311396384?mchid=1230000109");
HttpGet httpGet = new HttpGet(uriBuilder.build());
httpGet.addHeader("Accept", "application/json");

CloseableHttpResponse response = httpClient.execute(httpGet);

String bodyAsString = EntityUtils.toString(response.getEntity());
System.out.println(bodyAsString);

示例:关单

HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/1217752501201407033233368018/close");
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type","application/json; charset=utf-8");

ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectMapper objectMapper = new ObjectMapper();

ObjectNode rootNode = objectMapper.createObjectNode();
rootNode.put("mchid","1900009191");

objectMapper.writeValue(bos, rootNode);

httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
CloseableHttpResponse response = httpClient.execute(httpPost);

String bodyAsString = EntityUtils.toString(response.getEntity());
System.out.println(bodyAsString);

定制

当默认的本地签名和验签方式不适合你的系统时,你可以通过实现Signer或者Verifier来定制签名和验签。比如,你的系统把商户私钥集中存储,业务系统需通过远程调用进行签名,你可以这样做。

import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.Credentials;

// ...
Credentials credentials = new WechatPay2Credentials(merchantId, new Signer() {
  @Override
  public Signer.SignatureResult sign(byte[] message) {
    // ... call your sign-RPC, then return sign & serial number
  }
});
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
        .withCredentials(credentials)
        .withWechatPay(wechatpayPublicKeyId, wechatPayPublicKey);

定时更新平台证书功能

Important 重要

新注册的商户使用「微信支付公钥」验签,不需要下载和更新平台证书。仅尚未完全迁移至「微信支付公钥」验签的旧商户才需要此能力。

版本>=0.4.0可使用 CertificatesManager.getVerifier(merchantId) 得到的验签器替代默认的验签器。它会定时下载和更新商户对应的微信支付平台证书 (默认下载间隔为UPDATE_INTERVAL_MINUTE)。

示例代码:

// 获取证书管理器实例
certificatesManager = CertificatesManager.getInstance();
// 向证书管理器增加需要自动更新平台证书的商户信息
certificatesManager.putMerchant(merchantId, new WechatPay2Credentials(merchantId,
            new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)), apiV3Key.getBytes(StandardCharsets.UTF_8));
// ... 若有多个商户号,可继续调用putMerchant添加商户信息

// 从证书管理器中获取verifier
verifier = certificatesManager.getVerifier(merchantId);
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
        .withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
        .withValidator(new WechatPay2Validator(verifier));
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient

// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();

// 后面跟使用Apache HttpClient一样
CloseableHttpResponse response = httpClient.execute(...);

风险

因为不需要传入微信支付平台证书,CertificatesManager 在首次更新证书时不会验签,也就无法确认应答身份,可能导致下载错误的证书。

但下载时会通过 HTTPS、AES 对称加密来保证证书安全,所以可以认为,在使用官方 JDK、且 APIv3 密钥不泄露的情况下,CertificatesManager 是安全的。

敏感信息加解密

加密

使用 RsaCryptoUtil.encryptOAEP(String, PublicKey publicKey)进行公钥加密。示例代码如下。

// 建议从Verifier中获得微信支付平台证书,或使用预先下载到本地的平台证书文件中
PublicKey publicKey = verifier.getValidPublicKey();
try {
  String ciphertext = RsaCryptoUtil.encryptOAEP(text, publicKey);
} catch (IllegalBlockSizeException e) {
  e.printStackTrace();
}

解密

使用RsaCryptoUtil.decryptOAEP(String ciphertext, PrivateKey privateKey)进行私钥解密。示例代码如下。

// 使用商户私钥解密
try {
  String ciphertext = RsaCryptoUtil.decryptOAEP(text, merchantPrivateKey);
} catch (BadPaddingException e) {
  e.printStackTrace();
}

图片/视频上传

我们对上传的参数组装和签名逻辑进行了一定的封装,只需要以下几步:

  1. 使用WechatPayUploadHttpPost构造一个上传的HttpPost,需设置待上传文件的文件名,SHA256摘要,文件的输入流。在0.4.1及以上版本,支持设置媒体文件元信息。
  2. 通过WechatPayHttpClientBuilder得到的HttpClient发送请求。

示例请参考下列代码。

String filePath = "/your/home/hellokitty.png";
URI uri = new URI("https://api.mch.weixin.qq.com/v3/merchant/media/upload");
File file = new File(filePath);

try (FileInputStream ins1 = new FileInputStream(file)) {
  String sha256 = DigestUtils.sha256Hex(ins1);
  try (InputStream ins2 = new FileInputStream(file)) {
    HttpPost request = new WechatPayUploadHttpPost.Builder(uri)
// 如需直接设置媒体文件元信息,可使用withFile代替withImage
        .withImage(file.getName(), sha256, ins2)
        .build();
    CloseableHttpResponse response1 = httpClient.execute(request);
  }
}

AutoUpdateVerifierTest.uploadImageTest是一个更完整的示例。

回调通知的验签与解密

版本>=0.4.2可使用 NotificationHandler.parse(request) 对回调通知验签和解密:

  1. 使用NotificationRequest构造一个回调通知请求体,需设置应答平台证书序列号、应答随机串、应答时间戳、应答签名串、应答主体。
  2. 使用NotificationHandler构造一个回调通知处理器,需设置验证器、apiV3密钥。调用parse(request)得到回调通知notification。

示例请参考下列代码。

// 构建request,传入必要参数
NotificationRequest request = new NotificationRequest.Builder().withSerialNumber(wechatPaySerial)
        .withNonce(nonce)
        .withTimestamp(timestamp)
        .withSignature(signature)
        .withBody(body)
        .build();

// 如果已经初始化了 NotificationHandler 则直接使用,否则根据具体情况创建一个

// 1. 如果你使用的是微信支付公私钥,则使用公钥初始化 Verifier 以创建 NotificationHandler
NotificationHandler handler = new NotificationHandler(
        new PublicKeyVerifier(wechatPayPublicKeyId, wechatPayPublicKey),
        apiV3Key.getBytes(StandardCharsets.UTF_8)
);

// 2. 如果你使用的事微信支付平台证书,则从 CertificatesManager 获取 Verifier 以创建 NotificationHandler
NotificationHandler handler = new NotificationHandler(
        certificatesManager.getVerifier(merchantId),
        apiV3Key.getBytes(StandardCharsets.UTF_8)
);

// 3. 如果你正在进行微信支付平台证书到微信支付公私钥的灰度切换,希望保持切换兼容,则需要使用 MixVerifier 创建 NotificationHandler
Verifier mixVerifier = new MixVerifier(
        new PublicKeyVerifier(wechatPayPublicKeyId, wechatPayPublicKey),
        certificatesManager.getVerifier(merchantId)
);
NotificationHandler handler = new NotificationHandler(
        mixVerifier,
        apiV3Key.getBytes(StandardCharsets.UTF_8)
);

// 验签和解析请求体
Notification notification = handler.parse(request);
// 从notification中获取解密报文
System.out.println(notification.getDecryptData());

NotificationHandlerTest是一个更完整的示例。

异常处理

parse(request)可能返回以下异常,推荐对异常打日志或上报监控。

  • 抛出ValidationException时,请先检查传入参数是否与回调通知参数一致。若一致,说明参数可能被恶意篡改导致验签失败。
  • 抛出ParseException时,请先检查传入包体是否与回调通知包体一致。若一致,请检查AES密钥是否正确设置。若正确,说明包体可能被恶意篡改导致解析失败。

常见问题

如何加载商户私钥

商户申请商户API证书时,会生成商户私钥,并保存在本地证书文件夹的文件apiclient_key.pem中。商户开发者可以使用方法PemUtil.loadPrivateKey()加载证书。

// 示例:私钥存储在文件
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
        new FileInputStream("/path/to/apiclient_key.pem"));

// 示例:私钥为String字符串
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
        new ByteArrayInputStream(privateKey.getBytes("utf-8")));

如何下载平台证书?

使用WechatPayHttpClientBuilder需要调用withWechatPay设置微信支付平台证书,而平台证书又只能通过调用获取平台证书接口下载。为了解开"死循环",你可以在第一次下载平台证书时,按照下述方法临时"跳过”应答签名的验证。

CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create()
  .withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
  .withValidator(response -> true) // NOTE: 设置一个空的应答签名验证器,**不要**用在业务请求
  .build();

注意:业务请求请使用标准的初始化流程,务必验证应答签名。

如何下载账单

因为下载的账单文件可能会很大,为了平衡系统性能和签名验签的实现成本,账单下载API被分成了两个步骤:

  1. /v3/bill/tradebill 获取账单下载链接和账单摘要
  2. /v3/billdownload/file 账单文件下载,请求需签名但应答不签名

因为第二步不包含应答签名,我们可以参考上一个问题下载平台证书的方法,使用withValidator(response -> true)“跳过”应答的签名校验。

注意:开发者在下载文件之后,应使用第一步获取的账单摘要校验文件的完整性。

证书和回调解密需要的AesGcm解密在哪里?

请参考AesUtil.Java。

我想使用以前的版本,要怎么办

之前的版本可以从 jitpack 获取。例如希望使用0.1.6版本,gradle中可以使用以下的方式。

repositories {
    ...
    maven { url 'https://jitpack.io' }
}
...
dependencies {
    implementation 'com.github.wechatpay-apiv3:wechatpay-apache-httpclient:0.1.6'
    ...
}

如何解决Jackson NoSuchMethodError报错

在之前的版本中,我们出于安全考虑升级 Jackson 到2.12,并使用了2.11版本中新增的方法readValue(String src, Class<T> valueType)。如果你的项目所依赖的其他组件又依赖了低于2.11版本的 Jackson ,可能会出现依赖冲突。

我们建议有能力的开发者,升级冲突组件至较新的兼容版本。例如,issue #125 版本 <2.3.x 的 SpringBoot 官方已不再维护,继续使用可能会有安全隐患。

如果难以升级,你可以用下面的方式引入 jackson-bom 来升级 Jackson 版本。根据通用漏洞披露信息,我们推荐升级到2.13.2.20220328版本。

Gradle
implementation(platform("com.fasterxml.jackson:jackson-bom:2.13.2.20220328"))
Maven
<parent>
    <groupId>com.fasterxml.jackson</groupId>
    <artifactId>jackson-bom</artifactId>
    <version>2.13.2.20220328</version>
</parent>

如果出现其他组件的 NoSuchMethodError 报错,一般是依赖冲突导致。我们可以参考下面的解决思路:

  1. 从报错信息中找到出现问题的组件(如上面的 Jackson )。根据你的项目的构建方式,选择 Gradle 或 Maven 工具列出项目的依赖关系树,找到问题组件的所有版本号。
  2. 从报错信息中找到正确的组件版本号。一般来说,导致报错的原因是使用的组件版本太低,所以我们可以找组件在依赖关系树中最新的版本号。
  3. 指定组件版本。如果组件提供了 bom 依赖,可以使用上述方式引入 bom 依赖来指定版本。否则,根据你的项目的构建方式,选择 Gradle 或 Maven 的方式来指定版本。

更多常见问题

请看商户平台的常见问题,或者这里。

联系我们

如果你发现了BUG或者有任何疑问、建议,请通过issue进行反馈。

也欢迎访问我们的开发者社区。

评论

暂无评论