Java
更新时间:2025.12.04一、概述
本工具类 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.*; 4import com.google.gson.annotations.Expose; 5import com.google.gson.annotations.SerializedName; 6import java.util.List; 7import java.util.Map.Entry; 8import okhttp3.Headers; 9import okhttp3.Response; 10import okio.BufferedSource; 11import org.bouncycastle.crypto.digests.SM3Digest; 12import org.bouncycastle.jce.provider.BouncyCastleProvider; 13 14import javax.crypto.BadPaddingException; 15import javax.crypto.Cipher; 16import javax.crypto.IllegalBlockSizeException; 17import javax.crypto.NoSuchPaddingException; 18import javax.crypto.spec.GCMParameterSpec; 19import javax.crypto.spec.SecretKeySpec; 20import java.io.IOException; 21import java.io.InputStream; 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.*; 29import java.security.spec.InvalidKeySpecException; 30import java.security.spec.PKCS8EncodedKeySpec; 31import java.security.spec.X509EncodedKeySpec; 32import java.time.DateTimeException; 33import java.time.Duration; 34import java.time.Instant; 35import java.util.Base64; 36import java.util.HashMap; 37import java.util.Map; 38import java.util.Objects; 39 40public class WXPayBrandUtility { 41 private static final Gson gson = new GsonBuilder() 42 .disableHtmlEscaping() 43 .addSerializationExclusionStrategy(new ExclusionStrategy() { 44 @Override 45 public boolean shouldSkipField(FieldAttributes fieldAttributes) { 46 final Expose expose = fieldAttributes.getAnnotation(Expose.class); 47 return expose != null && !expose.serialize(); 48 } 49 50 @Override 51 public boolean shouldSkipClass(Class<?> aClass) { 52 return false; 53 } 54 }) 55 .addDeserializationExclusionStrategy(new ExclusionStrategy() { 56 @Override 57 public boolean shouldSkipField(FieldAttributes fieldAttributes) { 58 final Expose expose = fieldAttributes.getAnnotation(Expose.class); 59 return expose != null && !expose.deserialize(); 60 } 61 62 @Override 63 public boolean shouldSkipClass(Class<?> aClass) { 64 return false; 65 } 66 }) 67 .create(); 68 private static final char[] SYMBOLS = 69 "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); 70 private static final SecureRandom random = new SecureRandom(); 71 72 /** 73 * 将 Object 转换为 JSON 字符串 74 */ 75 public static String toJson(Object object) { 76 return gson.toJson(object); 77 } 78 79 /** 80 * 将 JSON 字符串解析为特定类型的实例 81 */ 82 public static <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException { 83 return gson.fromJson(json, classOfT); 84 } 85 86 /** 87 * 从公私钥文件路径中读取文件内容 88 * 89 * @param keyPath 文件路径 90 * @return 文件内容 91 */ 92 private static String readKeyStringFromPath(String keyPath) { 93 try { 94 return new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8); 95 } catch (IOException e) { 96 throw new UncheckedIOException(e); 97 } 98 } 99 100 /** 101 * 读取 PKCS#8 格式的私钥字符串并加载为私钥对象 102 * 103 * @param keyString 私钥文件内容,以 -----BEGIN PRIVATE KEY----- 开头 104 * @return PrivateKey 对象 105 */ 106 public static PrivateKey loadPrivateKeyFromString(String keyString) { 107 try { 108 keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "") 109 .replace("-----END PRIVATE KEY-----", "") 110 .replaceAll("\\s+", ""); 111 return KeyFactory.getInstance("RSA").generatePrivate( 112 new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString))); 113 } catch (NoSuchAlgorithmException e) { 114 throw new UnsupportedOperationException(e); 115 } catch (InvalidKeySpecException e) { 116 throw new IllegalArgumentException(e); 117 } 118 } 119 120 /** 121 * 从 PKCS#8 格式的私钥文件中加载私钥 122 * 123 * @param keyPath 私钥文件路径 124 * @return PrivateKey 对象 125 */ 126 public static PrivateKey loadPrivateKeyFromPath(String keyPath) { 127 return loadPrivateKeyFromString(readKeyStringFromPath(keyPath)); 128 } 129 130 /** 131 * 读取 PKCS#8 格式的公钥字符串并加载为公钥对象 132 * 133 * @param keyString 公钥文件内容,以 -----BEGIN PUBLIC KEY----- 开头 134 * @return PublicKey 对象 135 */ 136 public static PublicKey loadPublicKeyFromString(String keyString) { 137 try { 138 keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "") 139 .replace("-----END PUBLIC KEY-----", "") 140 .replaceAll("\\s+", ""); 141 return KeyFactory.getInstance("RSA").generatePublic( 142 new X509EncodedKeySpec(Base64.getDecoder().decode(keyString))); 143 } catch (NoSuchAlgorithmException e) { 144 throw new UnsupportedOperationException(e); 145 } catch (InvalidKeySpecException e) { 146 throw new IllegalArgumentException(e); 147 } 148 } 149 150 /** 151 * 从 PKCS#8 格式的公钥文件中加载公钥 152 * 153 * @param keyPath 公钥文件路径 154 * @return PublicKey 对象 155 */ 156 public static PublicKey loadPublicKeyFromPath(String keyPath) { 157 return loadPublicKeyFromString(readKeyStringFromPath(keyPath)); 158 } 159 160 /** 161 * 创建指定长度的随机字符串,字符集为[0-9a-zA-Z],可用于安全相关用途 162 */ 163 public static String createNonce(int length) { 164 char[] buf = new char[length]; 165 for (int i = 0; i < length; ++i) { 166 buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)]; 167 } 168 return new String(buf); 169 } 170 171 /** 172 * 使用公钥按照 RSA_PKCS1_OAEP_PADDING 算法进行加密 173 * 174 * @param publicKey 加密用公钥对象 175 * @param plaintext 待加密明文 176 * @return 加密后密文 177 */ 178 public static String encrypt(PublicKey publicKey, String plaintext) { 179 final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; 180 181 try { 182 Cipher cipher = Cipher.getInstance(transformation); 183 cipher.init(Cipher.ENCRYPT_MODE, publicKey); 184 return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8))); 185 } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { 186 throw new IllegalArgumentException("The current Java environment does not support " + transformation, e); 187 } catch (InvalidKeyException e) { 188 throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e); 189 } catch (BadPaddingException | IllegalBlockSizeException e) { 190 throw new IllegalArgumentException("Plaintext is too long", e); 191 } 192 } 193 194 /** 195 * 使用私钥按照 RSA_PKCS1_OAEP_PADDING 算法进行解密 196 * 197 * @param privateKey 解密用私钥对象 198 * @param ciphertext 待解密密文(Base64编码的字符串) 199 * @return 解密后明文 200 */ 201 public static String rsaOaepDecrypt(PrivateKey privateKey, String ciphertext) { 202 final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; 203 204 try { 205 Cipher cipher = Cipher.getInstance(transformation); 206 cipher.init(Cipher.DECRYPT_MODE, privateKey); 207 byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(ciphertext)); 208 return new String(decryptedBytes, StandardCharsets.UTF_8); 209 } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { 210 throw new IllegalArgumentException("The current Java environment does not support " + transformation, e); 211 } catch (InvalidKeyException e) { 212 throw new IllegalArgumentException("RSA decryption using an illegal privateKey", e); 213 } catch (BadPaddingException | IllegalBlockSizeException e) { 214 throw new IllegalArgumentException("Ciphertext decryption failed", e); 215 } 216 } 217 218 public static String aesAeadDecrypt(byte[] key, byte[] associatedData, byte[] nonce, 219 byte[] ciphertext) { 220 final String transformation = "AES/GCM/NoPadding"; 221 final String algorithm = "AES"; 222 final int tagLengthBit = 128; 223 224 try { 225 Cipher cipher = Cipher.getInstance(transformation); 226 cipher.init( 227 Cipher.DECRYPT_MODE, 228 new SecretKeySpec(key, algorithm), 229 new GCMParameterSpec(tagLengthBit, nonce)); 230 if (associatedData != null) { 231 cipher.updateAAD(associatedData); 232 } 233 return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8); 234 } catch (InvalidKeyException 235 | InvalidAlgorithmParameterException 236 | BadPaddingException 237 | IllegalBlockSizeException 238 | NoSuchAlgorithmException 239 | NoSuchPaddingException e) { 240 throw new IllegalArgumentException(String.format("AesAeadDecrypt with %s Failed", 241 transformation), e); 242 } 243 } 244 245 /** 246 * 使用私钥按照指定算法进行签名 247 * 248 * @param message 待签名串 249 * @param algorithm 签名算法,如 SHA256withRSA 250 * @param privateKey 签名用私钥对象 251 * @return 签名结果 252 */ 253 public static String sign(String message, String algorithm, PrivateKey privateKey) { 254 byte[] sign; 255 try { 256 Signature signature = Signature.getInstance(algorithm); 257 signature.initSign(privateKey); 258 signature.update(message.getBytes(StandardCharsets.UTF_8)); 259 sign = signature.sign(); 260 } catch (NoSuchAlgorithmException e) { 261 throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e); 262 } catch (InvalidKeyException e) { 263 throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e); 264 } catch (SignatureException e) { 265 throw new RuntimeException("An error occurred during the sign process.", e); 266 } 267 return Base64.getEncoder().encodeToString(sign); 268 } 269 270 /** 271 * 使用公钥按照特定算法验证签名 272 * 273 * @param message 待签名串 274 * @param signature 待验证的签名内容 275 * @param algorithm 签名算法,如:SHA256withRSA 276 * @param publicKey 验签用公钥对象 277 * @return 签名验证是否通过 278 */ 279 public static boolean verify(String message, String signature, String algorithm, 280 PublicKey publicKey) { 281 try { 282 Signature sign = Signature.getInstance(algorithm); 283 sign.initVerify(publicKey); 284 sign.update(message.getBytes(StandardCharsets.UTF_8)); 285 return sign.verify(Base64.getDecoder().decode(signature)); 286 } catch (SignatureException e) { 287 return false; 288 } catch (InvalidKeyException e) { 289 throw new IllegalArgumentException("verify uses an illegal publickey.", e); 290 } catch (NoSuchAlgorithmException e) { 291 throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e); 292 } 293 } 294 295 /** 296 * 根据品牌API请求签名规则构造 Authorization 签名 297 * 298 * @param brand_id 品牌ID 299 * @param certificateSerialNo 品牌API证书序列号 300 * @param privateKey 品牌API证书私钥 301 * @param method 请求接口的HTTP方法,请使用全大写表述,如 GET、POST、PUT、DELETE 302 * @param uri 请求接口的URL 303 * @param body 请求接口的Body 304 * @return 构造好的品牌API Authorization 头 305 */ 306 public static String buildAuthorization(String brand_id, String certificateSerialNo, 307 PrivateKey privateKey, 308 String method, String uri, String body) { 309 String nonce = createNonce(32); 310 long timestamp = Instant.now().getEpochSecond(); 311 312 String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce, 313 body == null ? "" : body); 314 315 String signature = sign(message, "SHA256withRSA", privateKey); 316 317 return String.format( 318 "WECHATPAY-BRAND-SHA256-RSA2048 brand_id=\"%s\",nonce_str=\"%s\",signature=\"%s\"," + 319 "timestamp=\"%d\",serial_no=\"%s\"", 320 brand_id, nonce, signature, timestamp, certificateSerialNo); 321 } 322 323 /** 324 * 计算输入流的哈希值 325 * 326 * @param inputStream 输入流 327 * @param algorithm 哈希算法名称,如 "SHA-256", "SHA-1" 328 * @return 哈希值的十六进制字符串 329 */ 330 private static String calculateHash(InputStream inputStream, String algorithm) { 331 try { 332 MessageDigest digest = MessageDigest.getInstance(algorithm); 333 byte[] buffer = new byte[8192]; 334 int bytesRead; 335 while ((bytesRead = inputStream.read(buffer)) != -1) { 336 digest.update(buffer, 0, bytesRead); 337 } 338 byte[] hashBytes = digest.digest(); 339 StringBuilder hexString = new StringBuilder(); 340 for (byte b : hashBytes) { 341 String hex = Integer.toHexString(0xff & b); 342 if (hex.length() == 1) { 343 hexString.append('0'); 344 } 345 hexString.append(hex); 346 } 347 return hexString.toString(); 348 } catch (NoSuchAlgorithmException e) { 349 throw new UnsupportedOperationException(algorithm + " algorithm not available", e); 350 } catch (IOException e) { 351 throw new RuntimeException("Error reading from input stream", e); 352 } 353 } 354 355 /** 356 * 计算输入流的 SHA256 哈希值 357 * 358 * @param inputStream 输入流 359 * @return SHA256 哈希值的十六进制字符串 360 */ 361 public static String sha256(InputStream inputStream) { 362 return calculateHash(inputStream, "SHA-256"); 363 } 364 365 /** 366 * 计算输入流的 SHA1 哈希值 367 * 368 * @param inputStream 输入流 369 * @return SHA1 哈希值的十六进制字符串 370 */ 371 public static String sha1(InputStream inputStream) { 372 return calculateHash(inputStream, "SHA-1"); 373 } 374 375 /** 376 * 计算输入流的 SM3 哈希值 377 * 378 * @param inputStream 输入流 379 * @return SM3 哈希值的十六进制字符串 380 */ 381 public static String sm3(InputStream inputStream) { 382 // 确保Bouncy Castle Provider已注册 383 if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { 384 Security.addProvider(new BouncyCastleProvider()); 385 } 386 387 try { 388 SM3Digest digest = new SM3Digest(); 389 byte[] buffer = new byte[8192]; 390 int bytesRead; 391 while ((bytesRead = inputStream.read(buffer)) != -1) { 392 digest.update(buffer, 0, bytesRead); 393 } 394 byte[] hashBytes = new byte[digest.getDigestSize()]; 395 digest.doFinal(hashBytes, 0); 396 397 StringBuilder hexString = new StringBuilder(); 398 for (byte b : hashBytes) { 399 String hex = Integer.toHexString(0xff & b); 400 if (hex.length() == 1) { 401 hexString.append('0'); 402 } 403 hexString.append(hex); 404 } 405 return hexString.toString(); 406 } catch (IOException e) { 407 throw new RuntimeException("Error reading from input stream", e); 408 } 409 } 410 411 /** 412 * 对参数进行 URL 编码 413 * 414 * @param content 参数内容 415 * @return 编码后的内容 416 */ 417 public static String urlEncode(String content) { 418 try { 419 return URLEncoder.encode(content, StandardCharsets.UTF_8.name()); 420 } catch (UnsupportedEncodingException e) { 421 throw new RuntimeException(e); 422 } 423 } 424 425 /** 426 * 对参数Map进行 URL 编码,生成 QueryString 427 * 428 * @param params Query参数Map 429 * @return QueryString 430 */ 431 public static String urlEncode(Map<String, Object> params) { 432 if (params == null || params.isEmpty()) { 433 return ""; 434 } 435 436 StringBuilder result = new StringBuilder(); 437 for (Entry<String, Object> entry : params.entrySet()) { 438 if (entry.getValue() == null) { 439 continue; 440 } 441 442 String key = entry.getKey(); 443 Object value = entry.getValue(); 444 if (value instanceof List) { 445 List<?> list = (List<?>) entry.getValue(); 446 for (Object temp : list) { 447 appendParam(result, key, temp); 448 } 449 } else { 450 appendParam(result, key, value); 451 } 452 } 453 return result.toString(); 454 } 455 456 /** 457 * 将键值对 放入返回结果 458 * 459 * @param result 返回的query string 460 * @param key 属性 461 * @param value 属性值 462 */ 463 private static void appendParam(StringBuilder result, String key, Object value) { 464 if (result.length() > 0) { 465 result.append("&"); 466 } 467 468 String valueString; 469 // 如果是基本类型、字符串或枚举,直接转换;如果是对象,序列化为JSON 470 if (value instanceof String || value instanceof Number || 471 value instanceof Boolean || value instanceof Enum) { 472 valueString = value.toString(); 473 } else { 474 valueString = toJson(value); 475 } 476 477 result.append(key) 478 .append("=") 479 .append(urlEncode(valueString)); 480 } 481 482 /** 483 * 从应答中提取 Body 484 * 485 * @param response HTTP 请求应答对象 486 * @return 应答中的Body内容,Body为空时返回空字符串 487 */ 488 public static String extractBody(Response response) { 489 if (response.body() == null) { 490 return ""; 491 } 492 493 try { 494 BufferedSource source = response.body().source(); 495 return source.readUtf8(); 496 } catch (IOException e) { 497 throw new RuntimeException(String.format("An error occurred during reading response body. " + 498 "Status: %d", response.code()), e); 499 } 500 } 501 502 /** 503 * 根据品牌API应答验签规则对应答签名进行验证,验证不通过时抛出异常 504 * 505 * @param wechatpayPublicKeyId 微信支付公钥ID 506 * @param wechatpayPublicKey 微信支付公钥对象 507 * @param headers 微信支付应答 Header 列表 508 * @param body 微信支付应答 Body 509 */ 510 public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey, 511 Headers headers, 512 String body) { 513 String timestamp = headers.get("Wechatpay-Timestamp"); 514 String requestId = headers.get("Request-ID"); 515 try { 516 Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp)); 517 // 拒绝过期请求 518 if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) { 519 throw new IllegalArgumentException( 520 String.format("Validate response failed, timestamp[%s] is expired, request-id[%s]", 521 timestamp, requestId)); 522 } 523 } catch (DateTimeException | NumberFormatException e) { 524 throw new IllegalArgumentException( 525 String.format("Validate response failed, timestamp[%s] is invalid, request-id[%s]", 526 timestamp, requestId)); 527 } 528 String serialNumber = headers.get("Wechatpay-Serial"); 529 if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) { 530 throw new IllegalArgumentException( 531 String.format("Validate response failed, Invalid Wechatpay-Serial, Local: %s, Remote: " + 532 "%s", wechatpayPublicKeyId, serialNumber)); 533 } 534 535 String signature = headers.get("Wechatpay-Signature"); 536 String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"), 537 body == null ? "" : body); 538 539 boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey); 540 if (!success) { 541 throw new IllegalArgumentException( 542 String.format("Validate response failed,the WechatPay signature is incorrect.%n" 543 + "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]", 544 headers.get("Request-ID"), headers, body)); 545 } 546 } 547 548 /** 549 * 根据品牌API通知验签规则对通知签名进行验证,验证不通过时抛出异常 550 * @param wechatpayPublicKeyId 微信支付公钥ID 551 * @param wechatpayPublicKey 微信支付公钥对象 552 * @param headers 微信支付通知 Header 列表 553 * @param body 微信支付通知 Body 554 */ 555 public static void validateNotification(String wechatpayPublicKeyId, 556 PublicKey wechatpayPublicKey, Headers headers, 557 String body) { 558 String timestamp = headers.get("Wechatpay-Timestamp"); 559 try { 560 Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp)); 561 // 拒绝过期请求 562 if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) { 563 throw new IllegalArgumentException( 564 String.format("Validate notification failed, timestamp[%s] is expired", timestamp)); 565 } 566 } catch (DateTimeException | NumberFormatException e) { 567 throw new IllegalArgumentException( 568 String.format("Validate notification failed, timestamp[%s] is invalid", timestamp)); 569 } 570 String serialNumber = headers.get("Wechatpay-Serial"); 571 if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) { 572 throw new IllegalArgumentException( 573 String.format("Validate notification failed, Invalid Wechatpay-Serial, Local: %s, " + 574 "Remote: %s", 575 wechatpayPublicKeyId, 576 serialNumber)); 577 } 578 579 String signature = headers.get("Wechatpay-Signature"); 580 String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"), 581 body == null ? "" : body); 582 583 boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey); 584 if (!success) { 585 throw new IllegalArgumentException( 586 String.format("Validate notification failed, WechatPay signature is incorrect.\n" 587 + "responseHeader[%s]\tresponseBody[%.1024s]", 588 headers, body)); 589 } 590 } 591 592 /** 593 * 对微信支付通知进行签名验证、解析,同时将业务数据解密。验签名失败、解析失败、解密失败时抛出异常 594 * @param apiv3Key 品牌API密钥 595 * @param wechatpayPublicKeyId 微信支付公钥ID 596 * @param wechatpayPublicKey 微信支付公钥对象 597 * @param headers 微信支付应答 Header 列表 598 * @param body 微信支付应答 Body 599 * @return 解析后的通知内容,解密后的业务数据可以使用 Notification.getPlaintext() 访问 600 */ 601 public static Notification parseNotification(String apiv3Key, String wechatpayPublicKeyId, 602 PublicKey wechatpayPublicKey, Headers headers, 603 String body) { 604 validateNotification(wechatpayPublicKeyId, wechatpayPublicKey, headers, body); 605 Notification notification = gson.fromJson(body, Notification.class); 606 notification.decrypt(apiv3Key); 607 return notification; 608 } 609 610 /** 611 * 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常 612 */ 613 public static class ApiException extends RuntimeException { 614 private static final long serialVersionUID = 2261086748874802175L; 615 616 private final int statusCode; 617 private final String body; 618 private final Headers headers; 619 private final String errorCode; 620 private final String errorMessage; 621 622 public ApiException(int statusCode, String body, Headers headers) { 623 super(String.format("微信支付API访问失败,StatusCode: [%s], Body: [%s], Headers: [%s]", statusCode, 624 body, headers)); 625 this.statusCode = statusCode; 626 this.body = body; 627 this.headers = headers; 628 629 if (body != null && !body.isEmpty()) { 630 JsonElement code; 631 JsonElement message; 632 633 try { 634 JsonObject jsonObject = gson.fromJson(body, JsonObject.class); 635 code = jsonObject.get("code"); 636 message = jsonObject.get("message"); 637 } catch (JsonSyntaxException ignored) { 638 code = null; 639 message = null; 640 } 641 this.errorCode = code == null ? null : code.getAsString(); 642 this.errorMessage = message == null ? null : message.getAsString(); 643 } else { 644 this.errorCode = null; 645 this.errorMessage = null; 646 } 647 } 648 649 /** 650 * 获取 HTTP 应答状态码 651 */ 652 public int getStatusCode() { 653 return statusCode; 654 } 655 656 /** 657 * 获取 HTTP 应答包体内容 658 */ 659 public String getBody() { 660 return body; 661 } 662 663 /** 664 * 获取 HTTP 应答 Header 665 */ 666 public Headers getHeaders() { 667 return headers; 668 } 669 670 /** 671 * 获取 错误码 (错误应答中的 code 字段) 672 */ 673 public String getErrorCode() { 674 return errorCode; 675 } 676 677 /** 678 * 获取 错误消息 (错误应答中的 message 字段) 679 */ 680 public String getErrorMessage() { 681 return errorMessage; 682 } 683 } 684 685 public static class Notification { 686 @SerializedName("id") 687 private String id; 688 @SerializedName("create_time") 689 private String createTime; 690 @SerializedName("event_type") 691 private String eventType; 692 @SerializedName("resource_type") 693 private String resourceType; 694 @SerializedName("summary") 695 private String summary; 696 @SerializedName("resource") 697 private Resource resource; 698 private String plaintext; 699 700 public String getId() { 701 return id; 702 } 703 704 public String getCreateTime() { 705 return createTime; 706 } 707 708 public String getEventType() { 709 return eventType; 710 } 711 712 public String getResourceType() { 713 return resourceType; 714 } 715 716 public String getSummary() { 717 return summary; 718 } 719 720 public Resource getResource() { 721 return resource; 722 } 723 724 /** 725 * 获取解密后的业务数据(JSON字符串,需要自行解析) 726 */ 727 public String getPlaintext() { 728 return plaintext; 729 } 730 731 private void validate() { 732 if (resource == null) { 733 throw new IllegalArgumentException("Missing required field `resource` in notification"); 734 } 735 resource.validate(); 736 } 737 738 /** 739 * 使用 APIv3Key 对通知中的业务数据解密,解密结果可以通过 getPlainText 访问。 740 * 外部拿到的 Notification 一定是解密过的,因此本方法没有设置为 public 741 * @param apiv3Key 品牌API密钥 742 */ 743 private void decrypt(String apiv3Key) { 744 validate(); 745 746 plaintext = aesAeadDecrypt( 747 apiv3Key.getBytes(StandardCharsets.UTF_8), 748 resource.associatedData.getBytes(StandardCharsets.UTF_8), 749 resource.nonce.getBytes(StandardCharsets.UTF_8), 750 Base64.getDecoder().decode(resource.ciphertext) 751 ); 752 } 753 754 public static class Resource { 755 @SerializedName("algorithm") 756 private String algorithm; 757 758 @SerializedName("ciphertext") 759 private String ciphertext; 760 761 @SerializedName("associated_data") 762 private String associatedData; 763 764 @SerializedName("nonce") 765 private String nonce; 766 767 @SerializedName("original_type") 768 private String originalType; 769 770 public String getAlgorithm() { 771 return algorithm; 772 } 773 774 public String getCiphertext() { 775 return ciphertext; 776 } 777 778 public String getAssociatedData() { 779 return associatedData; 780 } 781 782 public String getNonce() { 783 return nonce; 784 } 785 786 public String getOriginalType() { 787 return originalType; 788 } 789 790 private void validate() { 791 if (algorithm == null || algorithm.isEmpty()) { 792 throw new IllegalArgumentException("Missing required field `algorithm` in Notification" + 793 ".Resource"); 794 } 795 if (!Objects.equals(algorithm, "AEAD_AES_256_GCM")) { 796 throw new IllegalArgumentException(String.format("Unsupported `algorithm`[%s] in " + 797 "Notification.Resource", algorithm)); 798 } 799 800 if (ciphertext == null || ciphertext.isEmpty()) { 801 throw new IllegalArgumentException("Missing required field `ciphertext` in Notification" + 802 ".Resource"); 803 } 804 805 if (associatedData == null || associatedData.isEmpty()) { 806 throw new IllegalArgumentException("Missing required field `associatedData` in " + 807 "Notification.Resource"); 808 } 809 810 if (nonce == null || nonce.isEmpty()) { 811 throw new IllegalArgumentException("Missing required field `nonce` in Notification" + 812 ".Resource"); 813 } 814 815 if (originalType == null || originalType.isEmpty()) { 816 throw new IllegalArgumentException("Missing required field `originalType` in " + 817 "Notification.Resource"); 818 } 819 } 820 } 821 } 822 /** 823 * 根据文件名获取对应的Content-Type 824 * @param fileName 文件名 825 * @return Content-Type字符串 826 */ 827 public static String getContentTypeByFileName(String fileName) { 828 if (fileName == null || fileName.isEmpty()) { 829 return "application/octet-stream"; 830 } 831 832 // 获取文件扩展名 833 String extension = ""; 834 int lastDotIndex = fileName.lastIndexOf('.'); 835 if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1) { 836 extension = fileName.substring(lastDotIndex + 1).toLowerCase(); 837 } 838 839 // 常见文件类型映射 840 Map<String, String> contentTypeMap = new HashMap<>(); 841 // 图片类型 842 contentTypeMap.put("png", "image/png"); 843 contentTypeMap.put("jpg", "image/jpeg"); 844 contentTypeMap.put("jpeg", "image/jpeg"); 845 contentTypeMap.put("gif", "image/gif"); 846 contentTypeMap.put("bmp", "image/bmp"); 847 contentTypeMap.put("webp", "image/webp"); 848 contentTypeMap.put("svg", "image/svg+xml"); 849 contentTypeMap.put("ico", "image/x-icon"); 850 851 // 文档类型 852 contentTypeMap.put("pdf", "application/pdf"); 853 contentTypeMap.put("doc", "application/msword"); 854 contentTypeMap.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); 855 contentTypeMap.put("xls", "application/vnd.ms-excel"); 856 contentTypeMap.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); 857 contentTypeMap.put("ppt", "application/vnd.ms-powerpoint"); 858 contentTypeMap.put("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"); 859 860 // 文本类型 861 contentTypeMap.put("txt", "text/plain"); 862 contentTypeMap.put("html", "text/html"); 863 contentTypeMap.put("css", "text/css"); 864 contentTypeMap.put("js", "application/javascript"); 865 contentTypeMap.put("json", "application/json"); 866 contentTypeMap.put("xml", "application/xml"); 867 contentTypeMap.put("csv", "text/csv"); 868 869 870 // 音视频类型 871 contentTypeMap.put("mp3", "audio/mpeg"); 872 contentTypeMap.put("wav", "audio/wav"); 873 contentTypeMap.put("mp4", "video/mp4"); 874 contentTypeMap.put("avi", "video/x-msvideo"); 875 contentTypeMap.put("mov", "video/quicktime"); 876 877 // 压缩文件类型 878 contentTypeMap.put("zip", "application/zip"); 879 contentTypeMap.put("rar", "application/x-rar-compressed"); 880 contentTypeMap.put("7z", "application/x-7z-compressed"); 881 882 return contentTypeMap.getOrDefault(extension, "application/octet-stream"); 883 } 884}
文档是否有帮助

