diff --git a/src/main/java/com/wechat/pay/contrib/apache/httpclient/exception/ParseException.java b/src/main/java/com/wechat/pay/contrib/apache/httpclient/exception/ParseException.java new file mode 100644 index 0000000..884fdcb --- /dev/null +++ b/src/main/java/com/wechat/pay/contrib/apache/httpclient/exception/ParseException.java @@ -0,0 +1,17 @@ +package com.wechat.pay.contrib.apache.httpclient.exception; + +/** + * @author lianup + */ +public class ParseException extends WechatPayException { + + private static final long serialVersionUID = 4300538230471368120L; + + public ParseException(String message) { + super(message); + } + + public ParseException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/wechat/pay/contrib/apache/httpclient/exception/ValidationException.java b/src/main/java/com/wechat/pay/contrib/apache/httpclient/exception/ValidationException.java new file mode 100644 index 0000000..cabca21 --- /dev/null +++ b/src/main/java/com/wechat/pay/contrib/apache/httpclient/exception/ValidationException.java @@ -0,0 +1,15 @@ +package com.wechat.pay.contrib.apache.httpclient.exception; + +/** + * @author lianup + */ +public class ValidationException extends WechatPayException { + + + private static final long serialVersionUID = -3473204321736989263L; + + + public ValidationException(String message) { + super(message); + } +} diff --git a/src/main/java/com/wechat/pay/contrib/apache/httpclient/exception/WechatPayException.java b/src/main/java/com/wechat/pay/contrib/apache/httpclient/exception/WechatPayException.java index bcfbc7e..6d99a61 100644 --- a/src/main/java/com/wechat/pay/contrib/apache/httpclient/exception/WechatPayException.java +++ b/src/main/java/com/wechat/pay/contrib/apache/httpclient/exception/WechatPayException.java @@ -10,4 +10,9 @@ public abstract class WechatPayException extends Exception { public WechatPayException(String message) { super(message); } + + public WechatPayException(String message, Throwable cause) { + super(message, cause); + } + } diff --git a/src/main/java/com/wechat/pay/contrib/apache/httpclient/notification/Notification.java b/src/main/java/com/wechat/pay/contrib/apache/httpclient/notification/Notification.java new file mode 100644 index 0000000..f712b5c --- /dev/null +++ b/src/main/java/com/wechat/pay/contrib/apache/httpclient/notification/Notification.java @@ -0,0 +1,119 @@ +package com.wechat.pay.contrib.apache.httpclient.notification; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * 请求体解析结果 + * + * @author lianup + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Notification { + + @JsonProperty("id") + private String id; + @JsonProperty("create_time") + private String createTime; + @JsonProperty("event_type") + private String eventType; + @JsonProperty("resource_type") + private String resourceType; + @JsonProperty("summary") + private String summary; + @JsonProperty("resource") + private Resource resource; + private String decryptData; + + @Override + public String toString() { + return "Notification{" + + "id='" + id + '\'' + + ", createTime='" + createTime + '\'' + + ", eventType='" + eventType + '\'' + + ", resourceType='" + resourceType + '\'' + + ", decryptData='" + decryptData + '\'' + + ", summary='" + summary + '\'' + + ", resource=" + resource + + '}'; + } + + public String getId() { + return id; + } + + public String getCreateTime() { + return createTime; + } + + public String getEventType() { + return eventType; + } + + public String getDecryptData() { + return decryptData; + } + + public String getSummary() { + return summary; + } + + public String getResourceType() { + return resourceType; + } + + public Resource getResource() { + return resource; + } + + public void setDecryptData(String decryptData) { + this.decryptData = decryptData; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public class Resource { + + @JsonProperty("algorithm") + private String algorithm; + @JsonProperty("ciphertext") + private String ciphertext; + @JsonProperty("associated_data") + private String associatedData; + @JsonProperty("nonce") + private String nonce; + @JsonProperty("original_type") + private String originalType; + + public String getAlgorithm() { + return algorithm; + } + + public String getCiphertext() { + return ciphertext; + } + + public String getAssociatedData() { + return associatedData; + } + + public String getNonce() { + return nonce; + } + + public String getOriginalType() { + return originalType; + } + + @Override + public String toString() { + return "Resource{" + + "algorithm='" + algorithm + '\'' + + ", ciphertext='" + ciphertext + '\'' + + ", associatedData='" + associatedData + '\'' + + ", nonce='" + nonce + '\'' + + ", originalType='" + originalType + '\'' + + '}'; + } + } + +} diff --git a/src/main/java/com/wechat/pay/contrib/apache/httpclient/notification/NotificationHandler.java b/src/main/java/com/wechat/pay/contrib/apache/httpclient/notification/NotificationHandler.java new file mode 100644 index 0000000..174ef5b --- /dev/null +++ b/src/main/java/com/wechat/pay/contrib/apache/httpclient/notification/NotificationHandler.java @@ -0,0 +1,172 @@ +package com.wechat.pay.contrib.apache.httpclient.notification; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.wechat.pay.contrib.apache.httpclient.auth.Verifier; +import com.wechat.pay.contrib.apache.httpclient.exception.ParseException; +import com.wechat.pay.contrib.apache.httpclient.exception.ValidationException; +import com.wechat.pay.contrib.apache.httpclient.notification.Notification.Resource; +import com.wechat.pay.contrib.apache.httpclient.util.AesUtil; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; + +/** + * @author lianup + */ +public class NotificationHandler { + + private final Verifier verifier; + private final byte[] apiV3Key; + private static final ObjectMapper objectMapper = new ObjectMapper(); + +public NotificationHandler(Verifier verifier, byte[] apiV3Key) { + if (verifier == null) { + throw new IllegalArgumentException("verifier为空"); + } + if (apiV3Key == null || apiV3Key.length == 0) { + throw new IllegalArgumentException("apiV3Key为空"); + } + this.verifier = verifier; + this.apiV3Key = apiV3Key; +} + + /** + * 解析微信支付通知请求结果 + * + * @param request 微信支付通知请求 + * @return 微信支付通知报文解密结果 + * @throws ValidationException 1.输入参数不合法 2.参数被篡改导致验签失败 3.请求和验证的平台证书不一致导致验签失败 + * @throws ParseException 1.解析请求体为Json失败 2.请求体无对应参数 3.AES解密失败 + */ + public Notification parse(Request request) + throws ValidationException, ParseException { + // 验签 + validate(request); + // 解析请求体 + return parseBody(request.getBody()); + } + + private void validate(Request request) throws ValidationException { + if (request == null) { + throw new ValidationException("request为空"); + } + String serialNumber = request.getSerialNumber(); + byte[] message = request.getMessage(); + String signature = request.getSignature(); + if (serialNumber == null || serialNumber.isEmpty()) { + throw new ValidationException("serialNumber为空"); + } + if (message == null || message.length == 0) { + throw new ValidationException("message为空"); + } + if (signature == null || signature.isEmpty()) { + throw new ValidationException("signature为空"); + } + if (!verifier.verify(serialNumber, message, signature)) { + String errorMessage = String + .format("验签失败:serial=[%s] message=[%s] sign=[%s]", serialNumber, new String(message), signature); + throw new ValidationException(errorMessage); + } + } + + /** + * 解析请求体 + * + * @param body 请求体 + * @return 解析结果 + * @throws ParseException 解析body失败 + */ + private Notification parseBody(String body) throws ParseException { + ObjectReader objectReader = objectMapper.reader(); + Notification notification; + try { + notification = objectReader.readValue(body, Notification.class); + } catch (IOException ioException) { + throw new ParseException("解析body失败,body:" + body, ioException); + } + validateNotification(notification); + setDecryptData(notification); + return notification; + } + + /** + * 校验解析后的通知结果 + * + * @param notification 通知结果 + * @throws ParseException 参数不合法 + */ + private void validateNotification(Notification notification) throws ParseException { + if (notification == null) { + throw new ParseException("body解析为空"); + } + String id = notification.getId(); + if (id == null || id.isEmpty()) { + throw new ParseException("body不合法,id为空。body:" + notification.toString()); + } + String createTime = notification.getCreateTime(); + if (createTime == null || createTime.isEmpty()) { + throw new ParseException("body不合法,createTime为空。body:" + notification.toString()); + } + String eventType = notification.getEventType(); + if (eventType == null || eventType.isEmpty()) { + throw new ParseException("body不合法,eventType为空。body:" + notification.toString()); + } + String summary = notification.getSummary(); + if (summary == null || summary.isEmpty()) { + throw new ParseException("body不合法,summary为空。body:" + notification.toString()); + } + String resourceType = notification.getResourceType(); + if (resourceType == null || resourceType.isEmpty()) { + throw new ParseException("body不合法,resourceType为空。body:" + notification.toString()); + } + Resource resource = notification.getResource(); + if (resource == null) { + throw new ParseException("body不合法,resource为空。notification:" + notification.toString()); + } + String algorithm = resource.getAlgorithm(); + if (algorithm == null || algorithm.isEmpty()) { + throw new ParseException("body不合法,algorithm为空。body:" + notification.toString()); + } + String originalType = resource.getOriginalType(); + if (originalType == null || originalType.isEmpty()) { + throw new ParseException("body不合法,original_type为空。body:" + notification.toString()); + } + String ciphertext = resource.getCiphertext(); + if (ciphertext == null || ciphertext.isEmpty()) { + throw new ParseException("body不合法,ciphertext为空。body:" + notification.toString()); + } + String nonce = resource.getNonce(); + if (nonce == null || nonce.isEmpty()) { + throw new ParseException("body不合法,nonce为空。body:" + notification.toString()); + } + } + + /** + * 获取解密数据 + * + * @param notification 解析body得到的通知结果 + * @throws ParseException 解析body失败 + */ + private void setDecryptData(Notification notification) throws ParseException { + + Resource resource = notification.getResource(); + String getAssociateddData = ""; + if (resource.getAssociatedData() != null) { + getAssociateddData = resource.getAssociatedData(); + } + byte[] associatedData = getAssociateddData.getBytes(StandardCharsets.UTF_8); + byte[] nonce = resource.getNonce().getBytes(StandardCharsets.UTF_8); + String ciphertext = resource.getCiphertext(); + AesUtil aesUtil = new AesUtil(apiV3Key); + String decryptData; + try { + decryptData = aesUtil.decryptToString(associatedData, nonce, ciphertext); + } catch (GeneralSecurityException e) { + throw new ParseException("AES解密失败,resource:" + resource.toString(), e); + } + notification.setDecryptData(decryptData); + } + +} diff --git a/src/main/java/com/wechat/pay/contrib/apache/httpclient/notification/NotificationRequest.java b/src/main/java/com/wechat/pay/contrib/apache/httpclient/notification/NotificationRequest.java new file mode 100644 index 0000000..0177e3b --- /dev/null +++ b/src/main/java/com/wechat/pay/contrib/apache/httpclient/notification/NotificationRequest.java @@ -0,0 +1,88 @@ +package com.wechat.pay.contrib.apache.httpclient.notification; + +import java.nio.charset.StandardCharsets; + +/** + * @author lianup + */ +public class NotificationRequest implements Request { + + private final String serialNumber; + private final String signature; + private final byte[] message; + private final String body; + + private NotificationRequest(String serialNumber, String signature, byte[] message, String body) { + this.serialNumber = serialNumber; + this.signature = signature; + this.message = message; + this.body = body; + } + + @Override + public String getSerialNumber() { + return serialNumber; + } + + @Override + public byte[] getMessage() { + return message; + } + + @Override + public String getSignature() { + return signature; + } + + @Override + public String getBody() { + return body; + } + + public static class Builder { + + private String serialNumber; + private String timestamp; + private String nonce; + private String signature; + private String body; + + public Builder() { + } + + public Builder withSerialNumber(String serialNumber) { + this.serialNumber = serialNumber; + return this; + } + + public Builder withTimestamp(String timestamp) { + this.timestamp = timestamp; + return this; + } + + public Builder withNonce(String nonce) { + this.nonce = nonce; + return this; + } + + public Builder withSignature(String signature) { + this.signature = signature; + return this; + } + + public Builder withBody(String body) { + this.body = body; + return this; + } + + public NotificationRequest build() { + byte[] message = buildMessage(); + return new NotificationRequest(serialNumber, signature, message, body); + } + + private byte[] buildMessage() { + String verifyMessage = timestamp + "\n" + nonce + "\n" + body + "\n"; + return verifyMessage.getBytes(StandardCharsets.UTF_8); + } + } +} diff --git a/src/main/java/com/wechat/pay/contrib/apache/httpclient/notification/Request.java b/src/main/java/com/wechat/pay/contrib/apache/httpclient/notification/Request.java new file mode 100644 index 0000000..6a34330 --- /dev/null +++ b/src/main/java/com/wechat/pay/contrib/apache/httpclient/notification/Request.java @@ -0,0 +1,37 @@ +package com.wechat.pay.contrib.apache.httpclient.notification; + +/** + * 通知请求体,包含验签所需信息和报文体 + * + * @author lianup + */ +interface Request { + + /** + * 获取请求头Wechatpay-Serial + * + * @return serialNumber + */ + String getSerialNumber(); + + /** + * 获取验签串 + * + * @return message + */ + byte[] getMessage(); + + /** + * 获取请求头Wechatpay-Signature + * + * @return signature + */ + String getSignature(); + + /** + * 获取请求体 + * + * @return body + */ + String getBody(); +} diff --git a/src/test/java/com/wechat/pay/contrib/apache/httpclient/NotificationHandlerTest.java b/src/test/java/com/wechat/pay/contrib/apache/httpclient/NotificationHandlerTest.java new file mode 100644 index 0000000..3ca01ea --- /dev/null +++ b/src/test/java/com/wechat/pay/contrib/apache/httpclient/NotificationHandlerTest.java @@ -0,0 +1,59 @@ +package com.wechat.pay.contrib.apache.httpclient; + +import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner; +import com.wechat.pay.contrib.apache.httpclient.auth.Verifier; +import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials; +import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager; +import com.wechat.pay.contrib.apache.httpclient.notification.Notification; +import com.wechat.pay.contrib.apache.httpclient.notification.NotificationHandler; +import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest; +import com.wechat.pay.contrib.apache.httpclient.util.PemUtil; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class NotificationHandlerTest { + + private static final String privateKey = "-----BEGIN PRIVATE KEY-----\n" + + "-----END PRIVATE KEY-----\n"; // 商户私钥 + private static final String mchId = ""; // 商户号 + private static final String mchSerialNo = ""; // 商户证书序列号 + private static final String apiV3Key = ""; // apiV3密钥 + private static final String wechatPaySerial = ""; // 平台证书序列号 + private static final String nonce = ""; // 请求头Wechatpay-Nonce + private static final String timestamp = "";// 请求头Wechatpay-Timestamp + private static final String signature = "";// 请求头Wechatpay-Signature + private static final String body = ""; // 请求体 + private Verifier verifier; // 验签器 + private static CertificatesManager certificatesManager; // 平台证书管理器 + + @Before + public void setup() throws Exception { + PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(privateKey); + // 获取证书管理器实例 + certificatesManager = CertificatesManager.getInstance(); + // 向证书管理器增加需要自动更新平台证书的商户信息 + certificatesManager.putMerchant(mchId, new WechatPay2Credentials(mchId, + new PrivateKeySigner(mchSerialNo, merchantPrivateKey)), apiV3Key.getBytes(StandardCharsets.UTF_8)); + // 从证书管理器中获取verifier + verifier = certificatesManager.getVerifier(mchId); + } + + @Test + public void notificationHandlerTest() throws Exception { + // 构建request,传入必要参数 + NotificationRequest request = new NotificationRequest.Builder().withSerialNumber(wechatPaySerial) + .withNonce(nonce) + .withTimestamp(timestamp) + .withSignature(signature) + .withBody(body) + .build(); + NotificationHandler handler = new NotificationHandler(verifier, apiV3Key.getBytes(StandardCharsets.UTF_8)); + // 验签和解析请求体 + Notification notification = handler.parse(request); + Assert.assertNotNull(notification); + System.out.println(notification.toString()); + } +}