Java

更新时间:2025.05.27

一、概述

本工具类 WXPayUtility 为使用 Java 接入微信支付的开发者提供了一系列实用的功能,包括 JSON 处理、密钥加载、加密签名、请求头构建、响应验证等。通过使用这个工具类,开发者可以更方便地完成与微信支付相关的开发工作。

二、安装(引入依赖的第三方库)

本工具类依赖以下第三方库:

  1. Google Gson:用于 JSON 数据的序列化和反序列化。

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