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