WechatHttpClient.java
15.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
package com.diligrp.cashier.pipeline.client;
import com.diligrp.cashier.pipeline.domain.*;
import com.diligrp.cashier.pipeline.domain.wechat.*;
import com.diligrp.cashier.pipeline.exception.PaymentPipelineException;
import com.diligrp.cashier.pipeline.util.WechatConstants;
import com.diligrp.cashier.pipeline.util.WechatSignatureUtils;
import com.diligrp.cashier.shared.ErrorCode;
import com.diligrp.cashier.shared.service.ServiceEndpointSupport;
import com.diligrp.cashier.shared.util.DateUtils;
import com.diligrp.cashier.shared.util.JsonUtils;
import com.diligrp.cashier.shared.util.ObjectUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.nio.charset.StandardCharsets;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.time.Duration;
import java.util.*;
/**
* 微信支付基础功能HTTP客户端
*/
public class WechatHttpClient extends ServiceEndpointSupport {
private static final Logger LOG = LoggerFactory.getLogger(WechatHttpClient.class);
// 获取微信平台证书列表
private static final String LIST_CERTIFICATE = "/v3/certificates";
private static final String WECHAT_BASE_URL = "https://api.weixin.qq.com";
private static final String CODE_TO_SESSION = "/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code";
private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/stable_token";
private static final String UPLOAD_SHIPPING_URL = "https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info";
protected String wechatBaseUri;
protected WechatConfig wechatConfig;
public WechatHttpClient(String wechatBaseUri, WechatConfig wechatConfig) {
this.wechatBaseUri = wechatBaseUri;
this.wechatConfig = wechatConfig;
}
/**
* Native预支付下单, 返回二维码链接
*/
public NativePrepayResponse sendNativePrepayRequest(NativePrepayRequest request, String notifyUri) throws Exception {
throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "支付通道不支持Native支付");
}
/**
* 小程序支付预支付下单
*/
public String sendMiniProPrepayRequest(MiniProPrepayRequest request, String notifyUri) throws Exception {
throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "支付通道不支持JsApi支付");
}
/**
* 查询微信预支付订单状态
*/
public OnlinePaymentResponse queryPrepayResponse(OnlinePrepayOrder request) throws Exception {
throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "支付通道不支持此操作");
}
/**
* 关闭预支付订单
*/
public void closePrepayOrder(OnlinePrepayOrder request) throws Exception {
throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "支付通道不支持此操作");
}
/**
* 支付退款申请
*/
public OnlineRefundResponse sendRefundRequest(OnlineRefundRequest request, String notifyUri) throws Exception {
throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "支付通道不支持此操作");
}
/**
* 查询退款状态
*/
public OnlineRefundResponse queryRefundOrder(OnlineRefundOrder request) throws Exception {
throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "支付通道不支持此操作");
}
/**
* 刷新微信支付平台数字证书(公钥)
* 微信支付平台证书 - 当旧证书即将过期时,微信将新老证书将并行使用
* <a href="https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/wechatpay5_1.shtml">...</a>
* <a href="https://pay.weixin.qq.com/docs/merchant/apis/platform-certificate/api-v3-get-certificates/get.html">...</a>
*/
public void refreshCertificates() throws Exception {
// 获取认证信息和签名信息
String authorization = WechatSignatureUtils.authorization(wechatConfig.getMchId(), WechatConstants.HTTP_GET,
LIST_CERTIFICATE, wechatConfig.getPrivateKey(), wechatConfig.getSerialNo());
HttpRequest.Builder request = HttpRequest.newBuilder().uri(URI.create(wechatBaseUri + LIST_CERTIFICATE))
.version(HttpClient.Version.HTTP_2).timeout(Duration.ofMillis(MAX_REQUEST_TIMEOUT_TIME))
.header(CONTENT_TYPE, CONTENT_TYPE_JSON).header(WechatConstants.HEADER_AUTHORIZATION, authorization)
.header(WechatConstants.HEADER_ACCEPT, WechatConstants.ACCEPT_JSON)
.header(WechatConstants.HEADER_USER_AGENT, WechatConstants.USER_AGENT);
LOG.info("Sending wechat list certificate request...");
LOG.debug("Authorization: {}\n", authorization);
ServiceEndpointSupport.HttpResult result = execute(request.GET().build());
if (result.statusCode == 200) { // 200 处理成功有返回,204处理成功无返回; 返回成功时再进行数据验签,对数据无安全隐患
// 获取验签使用的微信平台证书序列号, 本地获取平台证书到则进行验签, 如获取不到则说明旧证书即将过期,新老证书正并行使用
String serialNo = result.header(WechatConstants.HEADER_SERIAL_NO);
String timestamp = result.header(WechatConstants.HEADER_TIMESTAMP);
String nonce = result.header(WechatConstants.HEADER_NONCE);
String sign = result.header(WechatConstants.HEADER_SIGNATURE);
LOG.debug("\n------Wechat Platform Data Verify------\nWechatpay-Serial={}\nWechatpay-Timestamp={}\n" +
"Wechatpay-Nonce={}\nWechatpay-Signature={}\n--------------------------------------", serialNo, timestamp, nonce, sign);
LOG.debug(result.responseText);
Optional<WechatCertificate> certificate = wechatConfig.getCertificate(serialNo);
if (certificate.isPresent()) {
boolean success = WechatSignatureUtils.verify(result.responseText, timestamp, nonce, sign,
certificate.get().getPublicKey());
if (!success) {
throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "获取微信支付平台证书验签失败");
}
} else {
LOG.warn("Old certificate is about to expire, new one is in use");
}
CertificateResponse response = JsonUtils.fromJsonString(result.responseText, CertificateResponse.class);
List<CertificateResponse.Certificate> certificates = response.getData();
if (ObjectUtils.isNotEmpty(certificates)) {
for (CertificateResponse.Certificate cert : certificates) {
// 利用ApiV3Key解密平台公钥
String certStr = WechatSignatureUtils.decrypt(cert.getEncrypt_certificate().getCiphertext(),
cert.getEncrypt_certificate().getNonce(), cert.getEncrypt_certificate().getAssociated_data(),
wechatConfig.getApiV3Key());
ByteArrayInputStream is = new ByteArrayInputStream(certStr.getBytes(StandardCharsets.UTF_8));
Certificate x509Cert = CertificateFactory.getInstance("X509").generateCertificate(is);
wechatConfig.putCertificate(WechatCertificate.of(cert.getSerial_no(), x509Cert.getPublicKey()));
LOG.info("{} certificate added", cert.getSerial_no());
}
}
LOG.info("Refresh certificate repository success");
} else {
LOG.info("Refresh certificate repository failed: {}", result.statusCode);
}
}
public String loginAuthorization(String code) {
return loginAuthorization(wechatConfig.getAppId(), wechatConfig.getAppSecret(), code);
}
/**
* 小程序登录授权,根据wx.login获得的临时登录凭证code,获取登录信息openId等
*/
public String loginAuthorization(String appId, String appSecret, String code) {
String uri = String.format(CODE_TO_SESSION, appId, appSecret, code);
HttpRequest.Builder request = HttpRequest.newBuilder().uri(URI.create(WECHAT_BASE_URL + uri))
.version(HttpClient.Version.HTTP_2).timeout(Duration.ofMillis(MAX_REQUEST_TIMEOUT_TIME))
.header(CONTENT_TYPE, CONTENT_TYPE_JSON).header(WechatConstants.HEADER_ACCEPT, WechatConstants.ACCEPT_JSON)
.header(WechatConstants.HEADER_USER_AGENT, WechatConstants.USER_AGENT);
LOG.info("Requesting wechat MiniPro login authorization info: {}\n{}", code, uri);
HttpResult result = execute(request.GET().build());
if (result.statusCode == 200) {
LOG.debug("Wechat MiniPro login authorization info response\n{}", result.responseText);
AuthorizationSession session = JsonUtils.fromJsonString(result.responseText, AuthorizationSession.class);
if (session.getErrcode() != null && session.getErrcode() != 0) {
LOG.error("Failed to request wechat MiniPro login authorization info: {}", session.getErrmsg());
throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "获取小程序登录授权信息失败: " + session.getErrmsg());
}
return session.getOpenid();
} else {
LOG.error("Failed to request wechat MiniPro login authorization info: {}", result.statusCode);
throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "获取小程序登录授权信息失败");
}
}
public WechatAccessToken getAccessToken() {
return getAccessToken(wechatConfig.getAppId(), wechatConfig.getAppSecret());
}
/**
* 获取小程序接口调用凭证:Token有效期内重复调用该接口不会更新Token,有效期5分钟前更新Token,新旧Token并行5分钟;该接口调用频率限制为 1万次每分钟,每天限制调用 50万次;
* @see <a href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getStableAccessToken.html">...</a>
*/
public WechatAccessToken getAccessToken(String appId, String appSecret) {
Map<String, Object> params = new HashMap<>();
params.put("grant_type", "client_credential");
params.put("appid", appId);
params.put("secret", appSecret);
params.put("force_refresh", Boolean.FALSE);
String payload = JsonUtils.toJsonString(params);
LOG.info("Requesting wechat MiniPro Api access token: {}", payload);
HttpResult result = send(ACCESS_TOKEN_URL, payload);
if (result.statusCode == 200) {
LOG.debug("Wechat MiniPro Api access token response: {}", result.responseText);
Map<String, Object> data = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
return WechatAccessToken.of((String)data.get("access_token"), (Integer) data.get("expires_in"));
} else {
LOG.error("Failed to request MiniPro Api access token: {}", result.statusCode);
throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "获取微信接口调用凭证失败");
}
}
/**
* 微信发货信息录入接口
* @see <a href="https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/order-shipping/order-shipping.html">...</a>
*/
public void sendUploadShippingRequest(UploadShippingRequest request, String accessToken) {
String uri = String.format("%s?access_token=%s", UPLOAD_SHIPPING_URL, accessToken);
Map<String, Object> orderKey = new HashMap<>();
orderKey.put("order_number_type", 2);
orderKey.put("transaction_id", request.getTransactionId());
Map<String, Object> shipping = new HashMap<>();
shipping.put("item_desc", request.getGoods());
List<Map<String, Object>> shippingList = new ArrayList<>();
shippingList.add(shipping);
Map<String, Object> payer = new HashMap<>();
payer.put("openid", request.getOpenId());
Map<String, Object> params = new HashMap<>();
params.put("order_key", orderKey);
params.put("logistics_type", request.getLogisticsType());
params.put("delivery_mode", 1);
params.put("shipping_list", shippingList);
params.put("upload_time", DateUtils.format(new Date(), "yyyy-MM-dd'T'HH:mm:ssZ"));
params.put("payer", payer);
String payload = JsonUtils.toJsonString(params);
LOG.info("Requesting wechat upload shipping: {}", payload);
HttpResult result = send(uri, payload);
if (result.statusCode == 200) {
LOG.debug("Wechat upload shipping response: {}", result.responseText);
Map<String, Object> data = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
int errorCode = (Integer) data.get("errcode");
if (errorCode != 0) {
String errorMessage = (String)data.get("errmsg");
throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "微信发货信息录入失败: " + errorMessage);
}
} else {
LOG.error("Failed to request wechat upload shipping: {}", result.statusCode);
throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "调用微信发货信息录入接口失败");
}
}
/**
* 用于微信支付结果通知数据验签
*/
public boolean dataVerify(String serialNo, String timestamp, String nonce, String sign, String payload) throws Exception {
LOG.debug("\n------Wechat Platform Data Verify------\nWechatpay-Serial={}\nWechatpay-Timestamp={}\n"
+ "Wechatpay-Nonce={}\nWechatpay-Signature={}\n{}\n--------------------------------------",
serialNo, timestamp, nonce, sign, payload == null ? "" : payload);
Optional<WechatCertificate> certOpt = wechatConfig.getCertificate(serialNo);
if (certOpt.isEmpty()) { // 找不到证书则重新向微信平台请求新证书(旧证书即将过期时出现)
refreshCertificates();
}
WechatCertificate certificate = wechatConfig.getCertificate(serialNo).orElseThrow(() ->
new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "找不到微信平台数字证书"));
return WechatSignatureUtils.verify(payload, timestamp, nonce, sign, certificate.getPublicKey());
}
protected void verifyHttpResult(HttpResult result) throws Exception {
if (result.statusCode == 401) {
return; // 微信签名失败,则不进行验签操作
}
// 获取验签使用的微信平台证书序列号, 本地获取平台证书到则进行验签, 如获取不到则说明旧证书即将过期,新老证书正并行使用
String serialNo = result.header(WechatConstants.HEADER_SERIAL_NO);
String timestamp = result.header(WechatConstants.HEADER_TIMESTAMP);
String nonce = result.header(WechatConstants.HEADER_NONCE);
String sign = result.header(WechatConstants.HEADER_SIGNATURE);
if (!dataVerify(serialNo, timestamp, nonce, sign, result.responseText)) {
throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "微信数据验签失败");
}
}
public WechatConfig getWechatConfig() {
return wechatConfig;
}
}