Java
更新时间:2025.05.27一、概述
本工具类 WXPayUtility 为使用 Java 接入微信支付的开发者提供了一系列实用的功能,包括 JSON 处理、密钥加载、加密签名、请求头构建、响应验证等。通过使用这个工具类,开发者可以更方便地完成与微信支付相关的开发工作。
二、安装(引入依赖的第三方库)
本工具类依赖以下第三方库:
Google Gson:用于 JSON 数据的序列化和反序列化。
OkHttp:用于 HTTP 请求处理。
你可以通过 Maven 或 Gradle 来引入这些依赖。
如果你使用的 Gradle,请在 build.gradle 中加入:
1implementation 'com.google.code.gson:gson:${VERSION}' 2implementation 'com.squareup.okhttp3:okhttp:${VERSION}'
如果你使用的 Maven,请在 pom.xml 中加入:
1<!-- Google Gson --> 2<dependency> 3 <groupId>com.google.code.gson</groupId> 4 <artifactId>gson</artifactId> 5 <version>${VERSION}</version> 6</dependency> 7<!-- OkHttp --> 8<dependency> 9 <groupId>com.squareup.okhttp3</groupId> 10 <artifactId>okhttp</artifactId> 11 <version>${VERSION}</version> 12</dependency>
三、必需的证书和密钥
运行 SDK 必需以下的商户身份信息,用于构造请求的签名和验证应答的签名:
四、工具类代码
1package com.java.utils; 2 3import com.google.gson.ExclusionStrategy; 4import com.google.gson.FieldAttributes; 5import com.google.gson.Gson; 6import com.google.gson.GsonBuilder; 7import com.google.gson.JsonElement; 8import com.google.gson.JsonObject; 9import com.google.gson.JsonParser; 10import com.google.gson.JsonSyntaxException; 11import com.google.gson.annotations.Expose; 12import com.wechat.pay.java.core.util.GsonUtil; 13import okhttp3.Headers; 14import okhttp3.Response; 15import okio.BufferedSource; 16 17import javax.crypto.BadPaddingException; 18import javax.crypto.Cipher; 19import javax.crypto.IllegalBlockSizeException; 20import javax.crypto.NoSuchPaddingException; 21import java.io.IOException; 22import java.io.UncheckedIOException; 23import java.io.UnsupportedEncodingException; 24import java.net.URLEncoder; 25import java.nio.charset.StandardCharsets; 26import java.nio.file.Files; 27import java.nio.file.Paths; 28import java.security.InvalidKeyException; 29import java.security.KeyFactory; 30import java.security.NoSuchAlgorithmException; 31import java.security.PrivateKey; 32import java.security.PublicKey; 33import java.security.SecureRandom; 34import java.security.Signature; 35import java.security.SignatureException; 36import java.security.spec.InvalidKeySpecException; 37import java.security.spec.PKCS8EncodedKeySpec; 38import java.security.spec.X509EncodedKeySpec; 39import java.time.DateTimeException; 40import java.time.Duration; 41import java.time.Instant; 42import java.util.Base64; 43import java.util.Map; 44import java.util.Objects; 45 46public class WXPayUtility { 47 private static final Gson gson = new GsonBuilder() 48 .disableHtmlEscaping() 49 .addSerializationExclusionStrategy(new ExclusionStrategy() { 50 @Override 51 public boolean shouldSkipField(FieldAttributes fieldAttributes) { 52 final Expose expose = fieldAttributes.getAnnotation(Expose.class); 53 return expose != null && !expose.serialize(); 54 } 55 56 @Override 57 public boolean shouldSkipClass(Class<?> aClass) { 58 return false; 59 } 60 }) 61 .addDeserializationExclusionStrategy(new ExclusionStrategy() { 62 @Override 63 public boolean shouldSkipField(FieldAttributes fieldAttributes) { 64 final Expose expose = fieldAttributes.getAnnotation(Expose.class); 65 return expose != null && !expose.deserialize(); 66 } 67 68 @Override 69 public boolean shouldSkipClass(Class<?> aClass) { 70 return false; 71 } 72 }) 73 .create(); 74 private static final char[] SYMBOLS = 75 "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); 76 private static final SecureRandom random = new SecureRandom(); 77 78 /** 79 * 将 Object 转换为 JSON 字符串 80 */ 81 public static String toJson(Object object) { 82 return gson.toJson(object); 83 } 84 85 /** 86 * 将 JSON 字符串解析为特定类型的实例 87 */ 88 public static <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException { 89 return gson.fromJson(json, classOfT); 90 } 91 92 /** 93 * 从公私钥文件路径中读取文件内容 94 * 95 * @param keyPath 文件路径 96 * @return 文件内容 97 */ 98 private static String readKeyStringFromPath(String keyPath) { 99 try { 100 return new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8); 101 } catch (IOException e) { 102 throw new UncheckedIOException(e); 103 } 104 } 105 106 /** 107 * 读取 PKCS#8 格式的私钥字符串并加载为私钥对象 108 * 109 * @param keyString 私钥文件内容,以 -----BEGIN PRIVATE KEY----- 开头 110 * @return PrivateKey 对象 111 */ 112 public static PrivateKey loadPrivateKeyFromString(String keyString) { 113 try { 114 keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "") 115 .replace("-----END PRIVATE KEY-----", "") 116 .replaceAll("\\s+", ""); 117 return KeyFactory.getInstance("RSA").generatePrivate( 118 new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString))); 119 } catch (NoSuchAlgorithmException e) { 120 throw new UnsupportedOperationException(e); 121 } catch (InvalidKeySpecException e) { 122 throw new IllegalArgumentException(e); 123 } 124 } 125 126 /** 127 * 从 PKCS#8 格式的私钥文件中加载私钥 128 * 129 * @param keyPath 私钥文件路径 130 * @return PrivateKey 对象 131 */ 132 public static PrivateKey loadPrivateKeyFromPath(String keyPath) { 133 return loadPrivateKeyFromString(readKeyStringFromPath(keyPath)); 134 } 135 136 /** 137 * 读取 PKCS#8 格式的公钥字符串并加载为公钥对象 138 * 139 * @param keyString 公钥文件内容,以 -----BEGIN PUBLIC KEY----- 开头 140 * @return PublicKey 对象 141 */ 142 public static PublicKey loadPublicKeyFromString(String keyString) { 143 try { 144 keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "") 145 .replace("-----END PUBLIC KEY-----", "") 146 .replaceAll("\\s+", ""); 147 return KeyFactory.getInstance("RSA").generatePublic( 148 new X509EncodedKeySpec(Base64.getDecoder().decode(keyString))); 149 } catch (NoSuchAlgorithmException e) { 150 throw new UnsupportedOperationException(e); 151 } catch (InvalidKeySpecException e) { 152 throw new IllegalArgumentException(e); 153 } 154 } 155 156 /** 157 * 从 PKCS#8 格式的公钥文件中加载公钥 158 * 159 * @param keyPath 公钥文件路径 160 * @return PublicKey 对象 161 */ 162 public static PublicKey loadPublicKeyFromPath(String keyPath) { 163 return loadPublicKeyFromString(readKeyStringFromPath(keyPath)); 164 } 165 166 /** 167 * 创建指定长度的随机字符串,字符集为[0-9a-zA-Z],可用于安全相关用途 168 */ 169 public static String createNonce(int length) { 170 char[] buf = new char[length]; 171 for (int i = 0; i < length; ++i) { 172 buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)]; 173 } 174 return new String(buf); 175 } 176 177 /** 178 * 使用公钥按照 RSA_PKCS1_OAEP_PADDING 算法进行加密 179 * 180 * @param publicKey 加密用公钥对象 181 * @param plaintext 待加密明文 182 * @return 加密后密文 183 */ 184 public static String encrypt(PublicKey publicKey, String plaintext) { 185 final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; 186 187 try { 188 Cipher cipher = Cipher.getInstance(transformation); 189 cipher.init(Cipher.ENCRYPT_MODE, publicKey); 190 return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8))); 191 } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { 192 throw new IllegalArgumentException("The current Java environment does not support " + transformation, e); 193 } catch (InvalidKeyException e) { 194 throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e); 195 } catch (BadPaddingException | IllegalBlockSizeException e) { 196 throw new IllegalArgumentException("Plaintext is too long", e); 197 } 198 } 199 200 /** 201 * 使用私钥按照指定算法进行签名 202 * 203 * @param message 待签名串 204 * @param algorithm 签名算法,如 SHA256withRSA 205 * @param privateKey 签名用私钥对象 206 * @return 签名结果 207 */ 208 public static String sign(String message, String algorithm, PrivateKey privateKey) { 209 byte[] sign; 210 try { 211 Signature signature = Signature.getInstance(algorithm); 212 signature.initSign(privateKey); 213 signature.update(message.getBytes(StandardCharsets.UTF_8)); 214 sign = signature.sign(); 215 } catch (NoSuchAlgorithmException e) { 216 throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e); 217 } catch (InvalidKeyException e) { 218 throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e); 219 } catch (SignatureException e) { 220 throw new RuntimeException("An error occurred during the sign process.", e); 221 } 222 return Base64.getEncoder().encodeToString(sign); 223 } 224 225 /** 226 * 使用公钥按照特定算法验证签名 227 * 228 * @param message 待签名串 229 * @param signature 待验证的签名内容 230 * @param algorithm 签名算法,如:SHA256withRSA 231 * @param publicKey 验签用公钥对象 232 * @return 签名验证是否通过 233 */ 234 public static boolean verify(String message, String signature, String algorithm, 235 PublicKey publicKey) { 236 try { 237 Signature sign = Signature.getInstance(algorithm); 238 sign.initVerify(publicKey); 239 sign.update(message.getBytes(StandardCharsets.UTF_8)); 240 return sign.verify(Base64.getDecoder().decode(signature)); 241 } catch (SignatureException e) { 242 return false; 243 } catch (InvalidKeyException e) { 244 throw new IllegalArgumentException("verify uses an illegal publickey.", e); 245 } catch (NoSuchAlgorithmException e) { 246 throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e); 247 } 248 } 249 250 /** 251 * 根据微信支付APIv3请求签名规则构造 Authorization 签名 252 * 253 * @param mchid 商户号 254 * @param certificateSerialNo 商户API证书序列号 255 * @param privateKey 商户API证书私钥 256 * @param method 请求接口的HTTP方法,请使用全大写表述,如 GET、POST、PUT、DELETE 257 * @param uri 请求接口的URL 258 * @param body 请求接口的Body 259 * @return 构造好的微信支付APIv3 Authorization 头 260 */ 261 public static String buildAuthorization(String mchid, String certificateSerialNo, 262 PrivateKey privateKey, 263 String method, String uri, String body) { 264 String nonce = createNonce(32); 265 long timestamp = Instant.now().getEpochSecond(); 266 267 String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce, 268 body == null ? "" : body); 269 270 String signature = sign(message, "SHA256withRSA", privateKey); 271 272 return String.format( 273 "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",signature=\"%s\"," + 274 "timestamp=\"%d\",serial_no=\"%s\"", 275 mchid, nonce, signature, timestamp, certificateSerialNo); 276 } 277 278 /** 279 * 对参数进行 URL 编码 280 * 281 * @param content 参数内容 282 * @return 编码后的内容 283 */ 284 public static String urlEncode(String content) { 285 try { 286 return URLEncoder.encode(content, StandardCharsets.UTF_8.name()); 287 } catch (UnsupportedEncodingException e) { 288 throw new RuntimeException(e); 289 } 290 } 291 292 /** 293 * 对参数Map进行 URL 编码,生成 QueryString 294 * 295 * @param params Query参数Map 296 * @return QueryString 297 */ 298 public static String urlEncode(Map<String, Object> params) { 299 if (params == null || params.isEmpty()) { 300 return ""; 301 } 302 303 int index = 0; 304 StringBuilder result = new StringBuilder(); 305 for (Map.Entry<String, Object> entry : params.entrySet()) { 306 result.append(entry.getKey()) 307 .append("=") 308 .append(urlEncode(entry.getValue().toString())); 309 index++; 310 if (index < params.size()) { 311 result.append("&"); 312 } 313 } 314 return result.toString(); 315 } 316 317 /** 318 * 从应答中提取 Body 319 * 320 * @param response HTTP 请求应答对象 321 * @return 应答中的Body内容,Body为空时返回空字符串 322 */ 323 public static String extractBody(Response response) { 324 if (response.body() == null) { 325 return ""; 326 } 327 328 try { 329 BufferedSource source = response.body().source(); 330 return source.readUtf8(); 331 } catch (IOException e) { 332 throw new RuntimeException(String.format("An error occurred during reading response body. Status: %d", response.code()), e); 333 } 334 } 335 336 /** 337 * 根据微信支付APIv3应答验签规则对应答签名进行验证,验证不通过时抛出异常 338 * 339 * @param wechatpayPublicKeyId 微信支付公钥ID 340 * @param wechatpayPublicKey 微信支付公钥对象 341 * @param headers 微信支付应答 Header 列表 342 * @param body 微信支付应答 Body 343 */ 344 public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey, 345 Headers headers, 346 String body) { 347 String timestamp = headers.get("Wechatpay-Timestamp"); 348 try { 349 Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp)); 350 // 拒绝过期请求 351 if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) { 352 throw new IllegalArgumentException( 353 String.format("Validate http response,timestamp[%s] of httpResponse is expires, " 354 + "request-id[%s]", 355 timestamp, headers.get("Request-ID"))); 356 } 357 } catch (DateTimeException | NumberFormatException e) { 358 throw new IllegalArgumentException( 359 String.format("Validate http response,timestamp[%s] of httpResponse is invalid, " + 360 "request-id[%s]", timestamp, 361 headers.get("Request-ID"))); 362 } 363 String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"), 364 body == null ? "" : body); 365 String serialNumber = headers.get("Wechatpay-Serial"); 366 if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) { 367 throw new IllegalArgumentException( 368 String.format("Invalid Wechatpay-Serial, Local: %s, Remote: %s", wechatpayPublicKeyId, 369 serialNumber)); 370 } 371 String signature = headers.get("Wechatpay-Signature"); 372 373 boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey); 374 if (!success) { 375 throw new IllegalArgumentException( 376 String.format("Validate response failed,the WechatPay signature is incorrect.%n" 377 + "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]", 378 headers.get("Request-ID"), headers, body)); 379 } 380 } 381 382 /** 383 * 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常 384 */ 385 public static class ApiException extends RuntimeException { 386 private static final long serialVersionUID = 2261086748874802175L; 387 388 private final int statusCode; 389 private final String body; 390 private final Headers headers; 391 private final String errorCode; 392 private final String errorMessage; 393 394 public ApiException(int statusCode, String body, Headers headers) { 395 super(String.format("微信支付API访问失败,StatusCode: [%s], Body: [%s], Headers: [%s]", statusCode, body, headers)); 396 this.statusCode = statusCode; 397 this.body = body; 398 this.headers = headers; 399 400 if (body != null && !body.isEmpty()) { 401 JsonElement code; 402 JsonElement message; 403 404 try { 405 JsonObject jsonObject = GsonUtil.getGson().fromJson(body, JsonObject.class); 406 code = jsonObject.get("code"); 407 message = jsonObject.get("message"); 408 } catch (JsonSyntaxException ignored) { 409 code = null; 410 message = null; 411 } 412 this.errorCode = code == null ? null : code.getAsString(); 413 this.errorMessage = message == null ? null : message.getAsString(); 414 } else { 415 this.errorCode = null; 416 this.errorMessage = null; 417 } 418 } 419 420 /** 421 * 获取 HTTP 应答状态码 422 */ 423 public int getStatusCode() { 424 return statusCode; 425 } 426 427 /** 428 * 获取 HTTP 应答包体内容 429 */ 430 public String getBody() { 431 return body; 432 } 433 434 /** 435 * 获取 HTTP 应答 Header 436 */ 437 public Headers getHeaders() { 438 return headers; 439 } 440 441 /** 442 * 获取 错误码 (错误应答中的 code 字段) 443 */ 444 public String getErrorCode() { 445 return errorCode; 446 } 447 448 /** 449 * 获取 错误消息 (错误应答中的 message 字段) 450 */ 451 public String getErrorMessage() { 452 return errorMessage; 453 } 454 } 455} 456
文档是否有帮助