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