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