Skip to content

Commit d2e41a5

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

File tree

8 files changed

+440
-0
lines changed

8 files changed

+440
-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: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package com.wechat.pay.contrib.apache.httpclient.notification;
2+
3+
4+
import com.fasterxml.jackson.core.JsonProcessingException;
5+
import com.fasterxml.jackson.databind.JsonNode;
6+
import com.fasterxml.jackson.databind.ObjectMapper;
7+
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
8+
import com.wechat.pay.contrib.apache.httpclient.exception.ParseException;
9+
import com.wechat.pay.contrib.apache.httpclient.exception.ValidationException;
10+
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
11+
import java.nio.charset.StandardCharsets;
12+
import java.security.GeneralSecurityException;
13+
14+
/**
15+
* @author lianup
16+
*/
17+
public class NotificationHandler {
18+
19+
private final Verifier verifier;
20+
private final byte[] apiV3Key;
21+
private static final ObjectMapper objectMapper = new ObjectMapper();
22+
23+
public NotificationHandler(Verifier verifier, byte[] apiV3Key) {
24+
if (verifier == null) {
25+
throw new IllegalArgumentException("verifier为空");
26+
}
27+
if (apiV3Key == null || apiV3Key.length == 0) {
28+
throw new IllegalArgumentException("apiV3Key为空");
29+
}
30+
this.verifier = verifier;
31+
this.apiV3Key = apiV3Key;
32+
}
33+
34+
/**
35+
* 解析微信支付通知请求结果
36+
*
37+
* @param request 微信支付通知请求
38+
* @return 微信支付通知报文解密结果
39+
* @throws ValidationException 验签失败
40+
* @throws ParseException 解析请求体失败
41+
*/
42+
public ParseResult parse(Request request)
43+
throws ValidationException, ParseException {
44+
// 验签
45+
validate(request);
46+
// 解析请求体
47+
return parseBody(request.getBody());
48+
}
49+
50+
private void validate(Request request) throws ValidationException {
51+
if (request == null) {
52+
throw new ValidationException("request为空");
53+
}
54+
String serialNumber = request.getSerialNumber();
55+
byte[] message = request.getMessage();
56+
String signature = request.getSignature();
57+
if (serialNumber == null || serialNumber.isEmpty()) {
58+
throw new ValidationException("serialNumber为空");
59+
}
60+
if (message == null || message.length == 0) {
61+
throw new ValidationException("message为空");
62+
}
63+
if (signature == null || signature.isEmpty()) {
64+
throw new ValidationException("signature为空");
65+
}
66+
if (!verifier.verify(serialNumber, message, signature)) {
67+
String errorMessage = String
68+
.format("验签失败:serial=[%s] message=[%s] sign=[%s]", serialNumber, new String(message), signature);
69+
throw new ValidationException(errorMessage);
70+
}
71+
}
72+
73+
/**
74+
* 解析请求体
75+
*
76+
* @param body 请求体
77+
* @return 解析结果
78+
* @throws ParseException 缺少ParseResult中的任意参数
79+
*/
80+
private ParseResult parseBody(String body) throws ParseException {
81+
JsonNode bodyNode = getBodyNode(body);
82+
String decryptData = getDecryptData(bodyNode);
83+
JsonNode idNode = bodyNode.get("id");
84+
JsonNode createTimeNode = bodyNode.get("create_time");
85+
JsonNode eventTypeNode = bodyNode.get("event_type");
86+
JsonNode summaryNode = bodyNode.get("summary");
87+
if (idNode == null || isEmpty(idNode)) {
88+
throw new ParseException("body不合法,id为空。body:" + body);
89+
}
90+
if (createTimeNode == null || isEmpty(createTimeNode)) {
91+
throw new ParseException("body不合法,create_time为空。body:" + body);
92+
}
93+
if (eventTypeNode == null || isEmpty(eventTypeNode)) {
94+
throw new ParseException("body不合法,event_type为空。body:" + body);
95+
}
96+
if (summaryNode == null || isEmpty(summaryNode)) {
97+
throw new ParseException("body不合法,summary为空。body:" + body);
98+
}
99+
String id = nodeToString(idNode);
100+
String createTime = nodeToString(createTimeNode);
101+
String eventType = nodeToString(eventTypeNode);
102+
String summary = nodeToString(summaryNode);
103+
return new ParseResult(id, createTime, eventType, decryptData, summary);
104+
}
105+
106+
private JsonNode getBodyNode(String body) throws ParseException {
107+
if (body == null || body.isEmpty()) {
108+
throw new ParseException("body为空");
109+
}
110+
JsonNode bodyNode;
111+
try {
112+
bodyNode = objectMapper.readTree(body);
113+
} catch (JsonProcessingException e) {
114+
throw new ParseException("body解析出错,body:" + body, e);
115+
}
116+
if (bodyNode == null) {
117+
throw new ParseException("body为空");
118+
}
119+
return bodyNode;
120+
}
121+
122+
private String nodeToString(JsonNode node) {
123+
return node.toString().replace("\"", "");
124+
}
125+
126+
private boolean isEmpty(JsonNode node) {
127+
return nodeToString(node).isEmpty();
128+
}
129+
130+
/**
131+
* 获取解密数据
132+
*
133+
* @param bodyNode 请求体Json节点
134+
* @return 解密数据
135+
* @throws ParseException 获取不到解密数据
136+
*/
137+
private String getDecryptData(JsonNode bodyNode) throws ParseException {
138+
JsonNode resourceNode = bodyNode.get("resource");
139+
String body = nodeToString(bodyNode);
140+
if (resourceNode == null) {
141+
throw new ParseException("body不合法,没有resource节点。body:" + body);
142+
}
143+
JsonNode nonceNode = resourceNode.get("nonce");
144+
JsonNode ciphertextNode = resourceNode.get("ciphertext");
145+
JsonNode associatedDataNode = resourceNode.get("associated_data");
146+
if (associatedDataNode == null) {
147+
throw new ParseException("body不合法,没有associated_data节点。body:" + body);
148+
}
149+
if (nonceNode == null || isEmpty(nonceNode)) {
150+
throw new ParseException("body不合法,nonce为空。body:" + body);
151+
}
152+
if (ciphertextNode == null || isEmpty(ciphertextNode)) {
153+
throw new ParseException("body不合法,没有ciphertext节点。body:" + body);
154+
}
155+
String getAssociatedData = nodeToString(associatedDataNode);
156+
byte[] associatedData = null;
157+
if (!getAssociatedData.isEmpty()) {
158+
associatedData = getAssociatedData.getBytes(StandardCharsets.UTF_8);
159+
}
160+
byte[] bodyNonce = nodeToString(nonceNode).getBytes(StandardCharsets.UTF_8);
161+
String ciphertext = nodeToString(ciphertextNode);
162+
AesUtil aesUtil = new AesUtil(apiV3Key);
163+
String decryptData;
164+
try {
165+
decryptData = aesUtil.decryptToString(associatedData, bodyNonce, ciphertext);
166+
} catch (GeneralSecurityException e) {
167+
String errorMessage = String.format("aes解密失败 apiV3Key[%s], associatedData[%s] bodyNonce[%s] ciphertext[%s]",
168+
new String(apiV3Key), getAssociatedData, new String(bodyNonce), ciphertext);
169+
throw new ParseException(errorMessage, e);
170+
}
171+
return decryptData;
172+
}
173+
}
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+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.wechat.pay.contrib.apache.httpclient.notification;
2+
3+
/**
4+
* 请求体解析结果类
5+
* 包括:通知ID、通知创建时间、解密通知数据、通知类型、回调摘要
6+
*/
7+
public class ParseResult {
8+
9+
private final String id;
10+
private final String createTime;
11+
private final String eventType;
12+
private final String decryptData;
13+
private final String summary;
14+
15+
public ParseResult(String id, String createTime, String eventType, String decryptData, String summary) {
16+
this.id = id;
17+
this.createTime = createTime;
18+
this.eventType = eventType;
19+
this.decryptData = decryptData;
20+
this.summary = summary;
21+
}
22+
23+
public String getId() {
24+
return id;
25+
}
26+
27+
public String getCreateTime() {
28+
return createTime;
29+
}
30+
31+
public String getEventType() {
32+
return eventType;
33+
}
34+
35+
public String getDecryptData() {
36+
return decryptData;
37+
}
38+
39+
public String getSummary() {
40+
return summary;
41+
}
42+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.wechat.pay.contrib.apache.httpclient.notification;
2+
3+
/**
4+
* 通知请求体,包含验签所需信息和报文体
5+
*
6+
* @author lianup
7+
*/
8+
interface Request {
9+
10+
/**
11+
* 获取请求头Wechatpay-Serial
12+
*
13+
* @return serialNumber
14+
*/
15+
String getSerialNumber();
16+
17+
/**
18+
* 获取验签串
19+
*
20+
* @return message
21+
*/
22+
byte[] getMessage();
23+
24+
/**
25+
* 获取请求头Wechatpay-Signature
26+
*
27+
* @return signature
28+
*/
29+
String getSignature();
30+
31+
/**
32+
* 获取请求体
33+
*
34+
* @return body
35+
*/
36+
String getBody();
37+
}

0 commit comments

Comments
 (0)