Skip to content

Commit 04e2e82

Browse files
committed
增加微信支付通知处理器
1 parent 9436ad9 commit 04e2e82

File tree

8 files changed

+468
-0
lines changed

8 files changed

+468
-0
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.wechat.pay.contrib.apache.httpclient.exception;
2+
3+
/**
4+
* @author lianup
5+
*/
6+
public class ParseException extends WechatPayException {
7+
8+
private static final long serialVersionUID = 4300538230471368120L;
9+
10+
public ParseException(String message) {
11+
super(message);
12+
}
13+
14+
public ParseException(String message, Throwable cause) {
15+
super(message, cause);
16+
}
17+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.wechat.pay.contrib.apache.httpclient.exception;
2+
3+
/**
4+
* @author lianup
5+
*/
6+
public class ValidationException extends WechatPayException {
7+
8+
9+
private static final long serialVersionUID = -3473204321736989263L;
10+
11+
12+
public ValidationException(String message) {
13+
super(message);
14+
}
15+
}

src/main/java/com/wechat/pay/contrib/apache/httpclient/exception/WechatPayException.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,9 @@ public abstract class WechatPayException extends Exception {
1010
public WechatPayException(String message) {
1111
super(message);
1212
}
13+
14+
public WechatPayException(String message, Throwable cause) {
15+
super(message, cause);
16+
}
17+
1318
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package com.wechat.pay.contrib.apache.httpclient.notification;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
6+
/**
7+
* 通知结果
8+
*
9+
* @author lianup
10+
*/
11+
@JsonIgnoreProperties(ignoreUnknown = true)
12+
public class Notification {
13+
14+
@JsonProperty("id")
15+
private String id;
16+
@JsonProperty("create_time")
17+
private String createTime;
18+
@JsonProperty("event_type")
19+
private String eventType;
20+
@JsonProperty("resource_type")
21+
private String resourceType;
22+
@JsonProperty("summary")
23+
private String summary;
24+
25+
public String getResourceType() {
26+
return resourceType;
27+
}
28+
29+
public Resource getResource() {
30+
return resource;
31+
}
32+
33+
@JsonProperty("resource")
34+
private Resource resource;
35+
private String decryptData;
36+
37+
@Override
38+
public String toString() {
39+
return "Notification{" +
40+
"id='" + id + '\'' +
41+
", createTime='" + createTime + '\'' +
42+
", eventType='" + eventType + '\'' +
43+
", resourceType='" + resourceType + '\'' +
44+
", decryptData='" + decryptData + '\'' +
45+
", summary='" + summary + '\'' +
46+
", resource=" + resource +
47+
'}';
48+
}
49+
50+
public String getId() {
51+
return id;
52+
}
53+
54+
public String getCreateTime() {
55+
return createTime;
56+
}
57+
58+
public String getEventType() {
59+
return eventType;
60+
}
61+
62+
public String getDecryptData() {
63+
return decryptData;
64+
}
65+
66+
public String getSummary() {
67+
return summary;
68+
}
69+
70+
public void setDecryptData(String decryptData) {
71+
this.decryptData = decryptData;
72+
}
73+
74+
@JsonIgnoreProperties(ignoreUnknown = true)
75+
public class Resource {
76+
77+
@JsonProperty("algorithm")
78+
private String algorithm;
79+
@JsonProperty("ciphertext")
80+
private String ciphertext;
81+
@JsonProperty("associated_data")
82+
private String associatedData;
83+
@JsonProperty("nonce")
84+
private String nonce;
85+
86+
public String getAlgorithm() {
87+
return algorithm;
88+
}
89+
90+
public String getCiphertext() {
91+
return ciphertext;
92+
}
93+
94+
public String getAssociatedData() {
95+
return associatedData;
96+
}
97+
98+
public String getNonce() {
99+
return nonce;
100+
}
101+
102+
@Override
103+
public String toString() {
104+
return "Resource{" +
105+
"algorithm='" + algorithm + '\'' +
106+
", ciphertext='" + ciphertext + '\'' +
107+
", associatedData='" + associatedData + '\'' +
108+
", nonce='" + nonce + '\'' +
109+
'}';
110+
}
111+
}
112+
113+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package com.wechat.pay.contrib.apache.httpclient.notification;
2+
3+
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.fasterxml.jackson.databind.ObjectReader;
6+
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
7+
import com.wechat.pay.contrib.apache.httpclient.exception.ParseException;
8+
import com.wechat.pay.contrib.apache.httpclient.exception.ValidationException;
9+
import com.wechat.pay.contrib.apache.httpclient.notification.Notification.Resource;
10+
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
11+
import java.io.IOException;
12+
import java.nio.charset.StandardCharsets;
13+
import java.security.GeneralSecurityException;
14+
15+
/**
16+
* @author lianup
17+
*/
18+
public class NotificationHandler {
19+
20+
private final Verifier verifier;
21+
private final byte[] apiV3Key;
22+
private static final ObjectMapper objectMapper = new ObjectMapper();
23+
24+
public NotificationHandler(Verifier verifier, byte[] apiV3Key) {
25+
if (verifier == null) {
26+
throw new IllegalArgumentException("verifier为空");
27+
}
28+
if (apiV3Key == null || apiV3Key.length == 0) {
29+
throw new IllegalArgumentException("apiV3Key为空");
30+
}
31+
this.verifier = verifier;
32+
this.apiV3Key = apiV3Key;
33+
}
34+
35+
/**
36+
* 解析微信支付通知请求结果
37+
*
38+
* @param request 微信支付通知请求
39+
* @return 微信支付通知报文解密结果
40+
* @throws ValidationException 验签失败
41+
* @throws ParseException 解析请求体失败
42+
*/
43+
public Notification parse(Request request)
44+
throws ValidationException, ParseException {
45+
// 验签
46+
validate(request);
47+
// 解析请求体
48+
return parseBody(request.getBody());
49+
}
50+
51+
private void validate(Request request) throws ValidationException {
52+
if (request == null) {
53+
throw new ValidationException("request为空");
54+
}
55+
String serialNumber = request.getSerialNumber();
56+
byte[] message = request.getMessage();
57+
String signature = request.getSignature();
58+
if (serialNumber == null || serialNumber.isEmpty()) {
59+
throw new ValidationException("serialNumber为空");
60+
}
61+
if (message == null || message.length == 0) {
62+
throw new ValidationException("message为空");
63+
}
64+
if (signature == null || signature.isEmpty()) {
65+
throw new ValidationException("signature为空");
66+
}
67+
if (!verifier.verify(serialNumber, message, signature)) {
68+
String errorMessage = String
69+
.format("验签失败:serial=[%s] message=[%s] sign=[%s]", serialNumber, new String(message), signature);
70+
throw new ValidationException(errorMessage);
71+
}
72+
}
73+
74+
/**
75+
* 解析请求体
76+
*
77+
* @param body 请求体
78+
* @return 解析结果
79+
* @throws ParseException 解析body失败
80+
*/
81+
private Notification parseBody(String body) throws ParseException {
82+
ObjectReader objectReader = objectMapper.reader();
83+
Notification notification;
84+
try {
85+
notification = objectReader.readValue(body, Notification.class);
86+
} catch (IOException ioException) {
87+
throw new ParseException("解析body失败,body:" + body, ioException);
88+
}
89+
setDecryptData(notification);
90+
return notification;
91+
}
92+
93+
/**
94+
* 获取解密数据
95+
*
96+
* @param notification 解析body得到的通知结果
97+
* @return 解密数据
98+
* @throws ParseException 解析body失败
99+
*/
100+
private String setDecryptData(Notification notification) throws ParseException {
101+
if (notification == null) {
102+
throw new ParseException("body解析为空。notification:" + notification.toString());
103+
}
104+
Resource resource = notification.getResource();
105+
if (resource == null) {
106+
throw new ParseException("body不合法,resource为空。notification:" + notification.toString());
107+
}
108+
String getAssociatedData = resource.getAssociatedData();
109+
String getBodyNonce = resource.getNonce();
110+
String ciphertext = resource.getCiphertext();
111+
if (getBodyNonce == null || getBodyNonce.isEmpty()) {
112+
throw new ParseException("body不合法,associated_data为空。notification:" + notification.toString());
113+
}
114+
if (ciphertext == null || ciphertext.isEmpty()) {
115+
throw new ParseException("body不合法,ciphertext为空。notification:" + notification.toString());
116+
}
117+
byte[] associatedData = null;
118+
if (!getAssociatedData.isEmpty()) {
119+
associatedData = getAssociatedData.getBytes(StandardCharsets.UTF_8);
120+
}
121+
byte[] bodyNonce = notification.getResource().getNonce().getBytes(StandardCharsets.UTF_8);
122+
AesUtil aesUtil = new AesUtil(apiV3Key);
123+
String decryptData;
124+
try {
125+
decryptData = aesUtil.decryptToString(associatedData, bodyNonce, ciphertext);
126+
} catch (GeneralSecurityException e) {
127+
String errorMessage = String.format("aes解密失败 apiV3Key[%s], associatedData[%s] bodyNonce[%s] ciphertext[%s]",
128+
new String(apiV3Key), getAssociatedData, new String(bodyNonce), ciphertext);
129+
throw new ParseException(errorMessage, e);
130+
}
131+
notification.setDecryptData(decryptData);
132+
return decryptData;
133+
}
134+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.wechat.pay.contrib.apache.httpclient.notification;
2+
3+
import java.nio.charset.StandardCharsets;
4+
5+
/**
6+
* @author lianup
7+
*/
8+
public class NotificationRequest implements Request {
9+
10+
private final String serialNumber;
11+
private final String signature;
12+
private final byte[] message;
13+
private final String body;
14+
15+
private NotificationRequest(String serialNumber, String signature, byte[] message, String body) {
16+
this.serialNumber = serialNumber;
17+
this.signature = signature;
18+
this.message = message;
19+
this.body = body;
20+
}
21+
22+
@Override
23+
public String getSerialNumber() {
24+
return serialNumber;
25+
}
26+
27+
@Override
28+
public byte[] getMessage() {
29+
return message;
30+
}
31+
32+
@Override
33+
public String getSignature() {
34+
return signature;
35+
}
36+
37+
@Override
38+
public String getBody() {
39+
return body;
40+
}
41+
42+
public static class Builder {
43+
44+
private String serialNumber;
45+
private String timestamp;
46+
private String nonce;
47+
private String signature;
48+
private String body;
49+
50+
public Builder() {
51+
}
52+
53+
public Builder withSerialNumber(String serialNumber) {
54+
this.serialNumber = serialNumber;
55+
return this;
56+
}
57+
58+
public Builder withTimestamp(String timestamp) {
59+
this.timestamp = timestamp;
60+
return this;
61+
}
62+
63+
public Builder withNonce(String nonce) {
64+
this.nonce = nonce;
65+
return this;
66+
}
67+
68+
public Builder withSignature(String signature) {
69+
this.signature = signature;
70+
return this;
71+
}
72+
73+
public Builder withBody(String body) {
74+
this.body = body;
75+
return this;
76+
}
77+
78+
public NotificationRequest build() {
79+
byte[] message = buildMessage();
80+
return new NotificationRequest(serialNumber, signature, message, body);
81+
}
82+
83+
private byte[] buildMessage() {
84+
String verifyMessage = timestamp + "\n" + nonce + "\n" + body + "\n";
85+
return verifyMessage.getBytes(StandardCharsets.UTF_8);
86+
}
87+
}
88+
}

0 commit comments

Comments
 (0)