Java
更新时间:2025.10.30一、概述
本工具类 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 public static String aesAeadDecrypt(byte[] key, byte[] associatedData, byte[] nonce, 195 byte[] ciphertext) { 196 final String transformation = "AES/GCM/NoPadding"; 197 final String algorithm = "AES"; 198 final int tagLengthBit = 128; 199 200 try { 201 Cipher cipher = Cipher.getInstance(transformation); 202 cipher.init( 203 Cipher.DECRYPT_MODE, 204 new SecretKeySpec(key, algorithm), 205 new GCMParameterSpec(tagLengthBit, nonce)); 206 if (associatedData != null) { 207 cipher.updateAAD(associatedData); 208 } 209 return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8); 210 } catch (InvalidKeyException 211 | InvalidAlgorithmParameterException 212 | BadPaddingException 213 | IllegalBlockSizeException 214 | NoSuchAlgorithmException 215 | NoSuchPaddingException e) { 216 throw new IllegalArgumentException(String.format("AesAeadDecrypt with %s Failed", 217 transformation), e); 218 } 219 } 220 221 /** 222 * 使用私钥按照指定算法进行签名 223 * 224 * @param message 待签名串 225 * @param algorithm 签名算法,如 SHA256withRSA 226 * @param privateKey 签名用私钥对象 227 * @return 签名结果 228 */ 229 public static String sign(String message, String algorithm, PrivateKey privateKey) { 230 byte[] sign; 231 try { 232 Signature signature = Signature.getInstance(algorithm); 233 signature.initSign(privateKey); 234 signature.update(message.getBytes(StandardCharsets.UTF_8)); 235 sign = signature.sign(); 236 } catch (NoSuchAlgorithmException e) { 237 throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e); 238 } catch (InvalidKeyException e) { 239 throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e); 240 } catch (SignatureException e) { 241 throw new RuntimeException("An error occurred during the sign process.", e); 242 } 243 return Base64.getEncoder().encodeToString(sign); 244 } 245 246 /** 247 * 使用公钥按照特定算法验证签名 248 * 249 * @param message 待签名串 250 * @param signature 待验证的签名内容 251 * @param algorithm 签名算法,如:SHA256withRSA 252 * @param publicKey 验签用公钥对象 253 * @return 签名验证是否通过 254 */ 255 public static boolean verify(String message, String signature, String algorithm, 256 PublicKey publicKey) { 257 try { 258 Signature sign = Signature.getInstance(algorithm); 259 sign.initVerify(publicKey); 260 sign.update(message.getBytes(StandardCharsets.UTF_8)); 261 return sign.verify(Base64.getDecoder().decode(signature)); 262 } catch (SignatureException e) { 263 return false; 264 } catch (InvalidKeyException e) { 265 throw new IllegalArgumentException("verify uses an illegal publickey.", e); 266 } catch (NoSuchAlgorithmException e) { 267 throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e); 268 } 269 } 270 271 /** 272 * 根据品牌API请求签名规则构造 Authorization 签名 273 * 274 * @param brand_id 品牌ID 275 * @param certificateSerialNo 品牌API证书序列号 276 * @param privateKey 品牌API证书私钥 277 * @param method 请求接口的HTTP方法,请使用全大写表述,如 GET、POST、PUT、DELETE 278 * @param uri 请求接口的URL 279 * @param body 请求接口的Body 280 * @return 构造好的品牌API Authorization 头 281 */ 282 public static String buildAuthorization(String brand_id, String certificateSerialNo, 283 PrivateKey privateKey, 284 String method, String uri, String body) { 285 String nonce = createNonce(32); 286 long timestamp = Instant.now().getEpochSecond(); 287 288 String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce, 289 body == null ? "" : body); 290 291 String signature = sign(message, "SHA256withRSA", privateKey); 292 293 return String.format( 294 "WECHATPAY-BRAND-SHA256-RSA2048 brand_id=\"%s\",nonce_str=\"%s\",signature=\"%s\"," + 295 "timestamp=\"%d\",serial_no=\"%s\"", 296 brand_id, nonce, signature, timestamp, certificateSerialNo); 297 } 298 299 /** 300 * 计算输入流的哈希值 301 * 302 * @param inputStream 输入流 303 * @param algorithm 哈希算法名称,如 "SHA-256", "SHA-1" 304 * @return 哈希值的十六进制字符串 305 */ 306 private static String calculateHash(InputStream inputStream, String algorithm) { 307 try { 308 MessageDigest digest = MessageDigest.getInstance(algorithm); 309 byte[] buffer = new byte[8192]; 310 int bytesRead; 311 while ((bytesRead = inputStream.read(buffer)) != -1) { 312 digest.update(buffer, 0, bytesRead); 313 } 314 byte[] hashBytes = digest.digest(); 315 StringBuilder hexString = new StringBuilder(); 316 for (byte b : hashBytes) { 317 String hex = Integer.toHexString(0xff & b); 318 if (hex.length() == 1) { 319 hexString.append('0'); 320 } 321 hexString.append(hex); 322 } 323 return hexString.toString(); 324 } catch (NoSuchAlgorithmException e) { 325 throw new UnsupportedOperationException(algorithm + " algorithm not available", e); 326 } catch (IOException e) { 327 throw new RuntimeException("Error reading from input stream", e); 328 } 329 } 330 331 /** 332 * 计算输入流的 SHA256 哈希值 333 * 334 * @param inputStream 输入流 335 * @return SHA256 哈希值的十六进制字符串 336 */ 337 public static String sha256(InputStream inputStream) { 338 return calculateHash(inputStream, "SHA-256"); 339 } 340 341 /** 342 * 计算输入流的 SHA1 哈希值 343 * 344 * @param inputStream 输入流 345 * @return SHA1 哈希值的十六进制字符串 346 */ 347 public static String sha1(InputStream inputStream) { 348 return calculateHash(inputStream, "SHA-1"); 349 } 350 351 /** 352 * 计算输入流的 SM3 哈希值 353 * 354 * @param inputStream 输入流 355 * @return SM3 哈希值的十六进制字符串 356 */ 357 public static String sm3(InputStream inputStream) { 358 // 确保Bouncy Castle Provider已注册 359 if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { 360 Security.addProvider(new BouncyCastleProvider()); 361 } 362 363 try { 364 SM3Digest digest = new SM3Digest(); 365 byte[] buffer = new byte[8192]; 366 int bytesRead; 367 while ((bytesRead = inputStream.read(buffer)) != -1) { 368 digest.update(buffer, 0, bytesRead); 369 } 370 byte[] hashBytes = new byte[digest.getDigestSize()]; 371 digest.doFinal(hashBytes, 0); 372 373 StringBuilder hexString = new StringBuilder(); 374 for (byte b : hashBytes) { 375 String hex = Integer.toHexString(0xff & b); 376 if (hex.length() == 1) { 377 hexString.append('0'); 378 } 379 hexString.append(hex); 380 } 381 return hexString.toString(); 382 } catch (IOException e) { 383 throw new RuntimeException("Error reading from input stream", e); 384 } 385 } 386 387 /** 388 * 对参数进行 URL 编码 389 * 390 * @param content 参数内容 391 * @return 编码后的内容 392 */ 393 public static String urlEncode(String content) { 394 try { 395 return URLEncoder.encode(content, StandardCharsets.UTF_8.name()); 396 } catch (UnsupportedEncodingException e) { 397 throw new RuntimeException(e); 398 } 399 } 400 401 /** 402 * 对参数Map进行 URL 编码,生成 QueryString 403 * 404 * @param params Query参数Map 405 * @return QueryString 406 */ 407 public static String urlEncode(Map<String, Object> params) { 408 if (params == null || params.isEmpty()) { 409 return ""; 410 } 411 412 StringBuilder result = new StringBuilder(); 413 for (Entry<String, Object> entry : params.entrySet()) { 414 if (entry.getValue() == null) { 415 continue; 416 } 417 418 String key = entry.getKey(); 419 Object value = entry.getValue(); 420 if (value instanceof List) { 421 List<?> list = (List<?>) entry.getValue(); 422 for (Object temp : list) { 423 appendParam(result, key, temp); 424 } 425 } else { 426 appendParam(result, key, value); 427 } 428 } 429 return result.toString(); 430 } 431 432 /** 433 * 将键值对 放入返回结果 434 * 435 * @param result 返回的query string 436 * @param key 属性 437 * @param value 属性值 438 */ 439 private static void appendParam(StringBuilder result, String key, Object value) { 440 if (result.length() > 0) { 441 result.append("&"); 442 } 443 444 String valueString; 445 // 如果是基本类型、字符串或枚举,直接转换;如果是对象,序列化为JSON 446 if (value instanceof String || value instanceof Number || 447 value instanceof Boolean || value instanceof Enum) { 448 valueString = value.toString(); 449 } else { 450 valueString = toJson(value); 451 } 452 453 result.append(key) 454 .append("=") 455 .append(urlEncode(valueString)); 456 } 457 458 /** 459 * 从应答中提取 Body 460 * 461 * @param response HTTP 请求应答对象 462 * @return 应答中的Body内容,Body为空时返回空字符串 463 */ 464 public static String extractBody(Response response) { 465 if (response.body() == null) { 466 return ""; 467 } 468 469 try { 470 BufferedSource source = response.body().source(); 471 return source.readUtf8(); 472 } catch (IOException e) { 473 throw new RuntimeException(String.format("An error occurred during reading response body. " + 474 "Status: %d", response.code()), e); 475 } 476 } 477 478 /** 479 * 根据品牌API应答验签规则对应答签名进行验证,验证不通过时抛出异常 480 * 481 * @param wechatpayPublicKeyId 微信支付公钥ID 482 * @param wechatpayPublicKey 微信支付公钥对象 483 * @param headers 微信支付应答 Header 列表 484 * @param body 微信支付应答 Body 485 */ 486 public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey, 487 Headers headers, 488 String body) { 489 String timestamp = headers.get("Wechatpay-Timestamp"); 490 String requestId = headers.get("Request-ID"); 491 try { 492 Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp)); 493 // 拒绝过期请求 494 if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) { 495 throw new IllegalArgumentException( 496 String.format("Validate response failed, timestamp[%s] is expired, request-id[%s]", 497 timestamp, requestId)); 498 } 499 } catch (DateTimeException | NumberFormatException e) { 500 throw new IllegalArgumentException( 501 String.format("Validate response failed, timestamp[%s] is invalid, request-id[%s]", 502 timestamp, requestId)); 503 } 504 String serialNumber = headers.get("Wechatpay-Serial"); 505 if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) { 506 throw new IllegalArgumentException( 507 String.format("Validate response failed, Invalid Wechatpay-Serial, Local: %s, Remote: " + 508 "%s", wechatpayPublicKeyId, serialNumber)); 509 } 510 511 String signature = headers.get("Wechatpay-Signature"); 512 String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"), 513 body == null ? "" : body); 514 515 boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey); 516 if (!success) { 517 throw new IllegalArgumentException( 518 String.format("Validate response failed,the WechatPay signature is incorrect.%n" 519 + "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]", 520 headers.get("Request-ID"), headers, body)); 521 } 522 } 523 524 /** 525 * 根据品牌API通知验签规则对通知签名进行验证,验证不通过时抛出异常 526 * @param wechatpayPublicKeyId 微信支付公钥ID 527 * @param wechatpayPublicKey 微信支付公钥对象 528 * @param headers 微信支付通知 Header 列表 529 * @param body 微信支付通知 Body 530 */ 531 public static void validateNotification(String wechatpayPublicKeyId, 532 PublicKey wechatpayPublicKey, Headers headers, 533 String body) { 534 String timestamp = headers.get("Wechatpay-Timestamp"); 535 try { 536 Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp)); 537 // 拒绝过期请求 538 if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) { 539 throw new IllegalArgumentException( 540 String.format("Validate notification failed, timestamp[%s] is expired", timestamp)); 541 } 542 } catch (DateTimeException | NumberFormatException e) { 543 throw new IllegalArgumentException( 544 String.format("Validate notification failed, timestamp[%s] is invalid", timestamp)); 545 } 546 String serialNumber = headers.get("Wechatpay-Serial"); 547 if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) { 548 throw new IllegalArgumentException( 549 String.format("Validate notification failed, Invalid Wechatpay-Serial, Local: %s, " + 550 "Remote: %s", 551 wechatpayPublicKeyId, 552 serialNumber)); 553 } 554 555 String signature = headers.get("Wechatpay-Signature"); 556 String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"), 557 body == null ? "" : body); 558 559 boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey); 560 if (!success) { 561 throw new IllegalArgumentException( 562 String.format("Validate notification failed, WechatPay signature is incorrect.\n" 563 + "responseHeader[%s]\tresponseBody[%.1024s]", 564 headers, body)); 565 } 566 } 567 568 /** 569 * 对微信支付通知进行签名验证、解析,同时将业务数据解密。验签名失败、解析失败、解密失败时抛出异常 570 * @param apiv3Key 品牌API密钥 571 * @param wechatpayPublicKeyId 微信支付公钥ID 572 * @param wechatpayPublicKey 微信支付公钥对象 573 * @param headers 微信支付应答 Header 列表 574 * @param body 微信支付应答 Body 575 * @return 解析后的通知内容,解密后的业务数据可以使用 Notification.getPlaintext() 访问 576 */ 577 public static Notification parseNotification(String apiv3Key, String wechatpayPublicKeyId, 578 PublicKey wechatpayPublicKey, Headers headers, 579 String body) { 580 validateNotification(wechatpayPublicKeyId, wechatpayPublicKey, headers, body); 581 Notification notification = gson.fromJson(body, Notification.class); 582 notification.decrypt(apiv3Key); 583 return notification; 584 } 585 586 /** 587 * 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常 588 */ 589 public static class ApiException extends RuntimeException { 590 private static final long serialVersionUID = 2261086748874802175L; 591 592 private final int statusCode; 593 private final String body; 594 private final Headers headers; 595 private final String errorCode; 596 private final String errorMessage; 597 598 public ApiException(int statusCode, String body, Headers headers) { 599 super(String.format("微信支付API访问失败,StatusCode: [%s], Body: [%s], Headers: [%s]", statusCode, 600 body, headers)); 601 this.statusCode = statusCode; 602 this.body = body; 603 this.headers = headers; 604 605 if (body != null && !body.isEmpty()) { 606 JsonElement code; 607 JsonElement message; 608 609 try { 610 JsonObject jsonObject = gson.fromJson(body, JsonObject.class); 611 code = jsonObject.get("code"); 612 message = jsonObject.get("message"); 613 } catch (JsonSyntaxException ignored) { 614 code = null; 615 message = null; 616 } 617 this.errorCode = code == null ? null : code.getAsString(); 618 this.errorMessage = message == null ? null : message.getAsString(); 619 } else { 620 this.errorCode = null; 621 this.errorMessage = null; 622 } 623 } 624 625 /** 626 * 获取 HTTP 应答状态码 627 */ 628 public int getStatusCode() { 629 return statusCode; 630 } 631 632 /** 633 * 获取 HTTP 应答包体内容 634 */ 635 public String getBody() { 636 return body; 637 } 638 639 /** 640 * 获取 HTTP 应答 Header 641 */ 642 public Headers getHeaders() { 643 return headers; 644 } 645 646 /** 647 * 获取 错误码 (错误应答中的 code 字段) 648 */ 649 public String getErrorCode() { 650 return errorCode; 651 } 652 653 /** 654 * 获取 错误消息 (错误应答中的 message 字段) 655 */ 656 public String getErrorMessage() { 657 return errorMessage; 658 } 659 } 660 661 public static class Notification { 662 @SerializedName("id") 663 private String id; 664 @SerializedName("create_time") 665 private String createTime; 666 @SerializedName("event_type") 667 private String eventType; 668 @SerializedName("resource_type") 669 private String resourceType; 670 @SerializedName("summary") 671 private String summary; 672 @SerializedName("resource") 673 private Resource resource; 674 private String plaintext; 675 676 public String getId() { 677 return id; 678 } 679 680 public String getCreateTime() { 681 return createTime; 682 } 683 684 public String getEventType() { 685 return eventType; 686 } 687 688 public String getResourceType() { 689 return resourceType; 690 } 691 692 public String getSummary() { 693 return summary; 694 } 695 696 public Resource getResource() { 697 return resource; 698 } 699 700 /** 701 * 获取解密后的业务数据(JSON字符串,需要自行解析) 702 */ 703 public String getPlaintext() { 704 return plaintext; 705 } 706 707 private void validate() { 708 if (resource == null) { 709 throw new IllegalArgumentException("Missing required field `resource` in notification"); 710 } 711 resource.validate(); 712 } 713 714 /** 715 * 使用 APIv3Key 对通知中的业务数据解密,解密结果可以通过 getPlainText 访问。 716 * 外部拿到的 Notification 一定是解密过的,因此本方法没有设置为 public 717 * @param apiv3Key 品牌API密钥 718 */ 719 private void decrypt(String apiv3Key) { 720 validate(); 721 722 plaintext = aesAeadDecrypt( 723 apiv3Key.getBytes(StandardCharsets.UTF_8), 724 resource.associatedData.getBytes(StandardCharsets.UTF_8), 725 resource.nonce.getBytes(StandardCharsets.UTF_8), 726 Base64.getDecoder().decode(resource.ciphertext) 727 ); 728 } 729 730 public static class Resource { 731 @SerializedName("algorithm") 732 private String algorithm; 733 734 @SerializedName("ciphertext") 735 private String ciphertext; 736 737 @SerializedName("associated_data") 738 private String associatedData; 739 740 @SerializedName("nonce") 741 private String nonce; 742 743 @SerializedName("original_type") 744 private String originalType; 745 746 public String getAlgorithm() { 747 return algorithm; 748 } 749 750 public String getCiphertext() { 751 return ciphertext; 752 } 753 754 public String getAssociatedData() { 755 return associatedData; 756 } 757 758 public String getNonce() { 759 return nonce; 760 } 761 762 public String getOriginalType() { 763 return originalType; 764 } 765 766 private void validate() { 767 if (algorithm == null || algorithm.isEmpty()) { 768 throw new IllegalArgumentException("Missing required field `algorithm` in Notification" + 769 ".Resource"); 770 } 771 if (!Objects.equals(algorithm, "AEAD_AES_256_GCM")) { 772 throw new IllegalArgumentException(String.format("Unsupported `algorithm`[%s] in " + 773 "Notification.Resource", algorithm)); 774 } 775 776 if (ciphertext == null || ciphertext.isEmpty()) { 777 throw new IllegalArgumentException("Missing required field `ciphertext` in Notification" + 778 ".Resource"); 779 } 780 781 if (associatedData == null || associatedData.isEmpty()) { 782 throw new IllegalArgumentException("Missing required field `associatedData` in " + 783 "Notification.Resource"); 784 } 785 786 if (nonce == null || nonce.isEmpty()) { 787 throw new IllegalArgumentException("Missing required field `nonce` in Notification" + 788 ".Resource"); 789 } 790 791 if (originalType == null || originalType.isEmpty()) { 792 throw new IllegalArgumentException("Missing required field `originalType` in " + 793 "Notification.Resource"); 794 } 795 } 796 } 797 } 798 /** 799 * 根据文件名获取对应的Content-Type 800 * @param fileName 文件名 801 * @return Content-Type字符串 802 */ 803 public static String getContentTypeByFileName(String fileName) { 804 if (fileName == null || fileName.isEmpty()) { 805 return "application/octet-stream"; 806 } 807 808 // 获取文件扩展名 809 String extension = ""; 810 int lastDotIndex = fileName.lastIndexOf('.'); 811 if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1) { 812 extension = fileName.substring(lastDotIndex + 1).toLowerCase(); 813 } 814 815 // 常见文件类型映射 816 Map<String, String> contentTypeMap = new HashMap<>(); 817 // 图片类型 818 contentTypeMap.put("png", "image/png"); 819 contentTypeMap.put("jpg", "image/jpeg"); 820 contentTypeMap.put("jpeg", "image/jpeg"); 821 contentTypeMap.put("gif", "image/gif"); 822 contentTypeMap.put("bmp", "image/bmp"); 823 contentTypeMap.put("webp", "image/webp"); 824 contentTypeMap.put("svg", "image/svg+xml"); 825 contentTypeMap.put("ico", "image/x-icon"); 826 827 // 文档类型 828 contentTypeMap.put("pdf", "application/pdf"); 829 contentTypeMap.put("doc", "application/msword"); 830 contentTypeMap.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); 831 contentTypeMap.put("xls", "application/vnd.ms-excel"); 832 contentTypeMap.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); 833 contentTypeMap.put("ppt", "application/vnd.ms-powerpoint"); 834 contentTypeMap.put("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"); 835 836 // 文本类型 837 contentTypeMap.put("txt", "text/plain"); 838 contentTypeMap.put("html", "text/html"); 839 contentTypeMap.put("css", "text/css"); 840 contentTypeMap.put("js", "application/javascript"); 841 contentTypeMap.put("json", "application/json"); 842 contentTypeMap.put("xml", "application/xml"); 843 contentTypeMap.put("csv", "text/csv"); 844 845 846 // 音视频类型 847 contentTypeMap.put("mp3", "audio/mpeg"); 848 contentTypeMap.put("wav", "audio/wav"); 849 contentTypeMap.put("mp4", "video/mp4"); 850 contentTypeMap.put("avi", "video/x-msvideo"); 851 contentTypeMap.put("mov", "video/quicktime"); 852 853 // 压缩文件类型 854 contentTypeMap.put("zip", "application/zip"); 855 contentTypeMap.put("rar", "application/x-rar-compressed"); 856 contentTypeMap.put("7z", "application/x-7z-compressed"); 857 858 return contentTypeMap.getOrDefault(extension, "application/octet-stream"); 859 } 860}
文档是否有帮助

