Skip to content

Commit c5d89c7

Browse files
authored
增加微信支付通知处理器 (#105)
1 parent 9436ad9 commit c5d89c7

File tree

8 files changed

+512
-0
lines changed

8 files changed

+512
-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: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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+
@JsonProperty("resource")
25+
private Resource resource;
26+
private String decryptData;
27+
28+
@Override
29+
public String toString() {
30+
return "Notification{" +
31+
"id='" + id + '\'' +
32+
", createTime='" + createTime + '\'' +
33+
", eventType='" + eventType + '\'' +
34+
", resourceType='" + resourceType + '\'' +
35+
", decryptData='" + decryptData + '\'' +
36+
", summary='" + summary + '\'' +
37+
", resource=" + resource +
38+
'}';
39+
}
40+
41+
public String getId() {
42+
return id;
43+
}
44+
45+
public String getCreateTime() {
46+
return createTime;
47+
}
48+
49+
public String getEventType() {
50+
return eventType;
51+
}
52+
53+
public String getDecryptData() {
54+
return decryptData;
55+
}
56+
57+
public String getSummary() {
58+
return summary;
59+
}
60+
61+
public String getResourceType() {
62+
return resourceType;
63+
}
64+
65+
public Resource getResource() {
66+
return resource;
67+
}
68+
69+
public void setDecryptData(String decryptData) {
70+
this.decryptData = decryptData;
71+
}
72+
73+
@JsonIgnoreProperties(ignoreUnknown = true)
74+
public class Resource {
75+
76+
@JsonProperty("algorithm")
77+
private String algorithm;
78+
@JsonProperty("ciphertext")
79+
private String ciphertext;
80+
@JsonProperty("associated_data")
81+
private String associatedData;
82+
@JsonProperty("nonce")
83+
private String nonce;
84+
@JsonProperty("original_type")
85+
private String originalType;
86+
87+
public String getAlgorithm() {
88+
return algorithm;
89+
}
90+
91+
public String getCiphertext() {
92+
return ciphertext;
93+
}
94+
95+
public String getAssociatedData() {
96+
return associatedData;
97+
}
98+
99+
public String getNonce() {
100+
return nonce;
101+
}
102+
103+
public String getOriginalType() {
104+
return originalType;
105+
}
106+
107+
@Override
108+
public String toString() {
109+
return "Resource{" +
110+
"algorithm='" + algorithm + '\'' +
111+
", ciphertext='" + ciphertext + '\'' +
112+
", associatedData='" + associatedData + '\'' +
113+
", nonce='" + nonce + '\'' +
114+
", originalType='" + originalType + '\'' +
115+
'}';
116+
}
117+
}
118+
119+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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 1.输入参数不合法 2.参数被篡改导致验签失败 3.请求和验证的平台证书不一致导致验签失败
41+
* @throws ParseException 1.解析请求体为Json失败 2.请求体无对应参数 3.AES解密失败
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+
validateNotification(notification);
90+
setDecryptData(notification);
91+
return notification;
92+
}
93+
94+
/**
95+
* 校验解析后的通知结果
96+
*
97+
* @param notification 通知结果
98+
* @throws ParseException 参数不合法
99+
*/
100+
private void validateNotification(Notification notification) throws ParseException {
101+
if (notification == null) {
102+
throw new ParseException("body解析为空");
103+
}
104+
String id = notification.getId();
105+
if (id == null || id.isEmpty()) {
106+
throw new ParseException("body不合法,id为空。body:" + notification.toString());
107+
}
108+
String createTime = notification.getCreateTime();
109+
if (createTime == null || createTime.isEmpty()) {
110+
throw new ParseException("body不合法,createTime为空。body:" + notification.toString());
111+
}
112+
String eventType = notification.getEventType();
113+
if (eventType == null || eventType.isEmpty()) {
114+
throw new ParseException("body不合法,eventType为空。body:" + notification.toString());
115+
}
116+
String summary = notification.getSummary();
117+
if (summary == null || summary.isEmpty()) {
118+
throw new ParseException("body不合法,summary为空。body:" + notification.toString());
119+
}
120+
String resourceType = notification.getResourceType();
121+
if (resourceType == null || resourceType.isEmpty()) {
122+
throw new ParseException("body不合法,resourceType为空。body:" + notification.toString());
123+
}
124+
Resource resource = notification.getResource();
125+
if (resource == null) {
126+
throw new ParseException("body不合法,resource为空。notification:" + notification.toString());
127+
}
128+
String algorithm = resource.getAlgorithm();
129+
if (algorithm == null || algorithm.isEmpty()) {
130+
throw new ParseException("body不合法,algorithm为空。body:" + notification.toString());
131+
}
132+
String originalType = resource.getOriginalType();
133+
if (originalType == null || originalType.isEmpty()) {
134+
throw new ParseException("body不合法,original_type为空。body:" + notification.toString());
135+
}
136+
String ciphertext = resource.getCiphertext();
137+
if (ciphertext == null || ciphertext.isEmpty()) {
138+
throw new ParseException("body不合法,ciphertext为空。body:" + notification.toString());
139+
}
140+
String nonce = resource.getNonce();
141+
if (nonce == null || nonce.isEmpty()) {
142+
throw new ParseException("body不合法,nonce为空。body:" + notification.toString());
143+
}
144+
}
145+
146+
/**
147+
* 获取解密数据
148+
*
149+
* @param notification 解析body得到的通知结果
150+
* @throws ParseException 解析body失败
151+
*/
152+
private void setDecryptData(Notification notification) throws ParseException {
153+
154+
Resource resource = notification.getResource();
155+
String getAssociateddData = "";
156+
if (resource.getAssociatedData() != null) {
157+
getAssociateddData = resource.getAssociatedData();
158+
}
159+
byte[] associatedData = getAssociateddData.getBytes(StandardCharsets.UTF_8);
160+
byte[] nonce = resource.getNonce().getBytes(StandardCharsets.UTF_8);
161+
String ciphertext = resource.getCiphertext();
162+
AesUtil aesUtil = new AesUtil(apiV3Key);
163+
String decryptData;
164+
try {
165+
decryptData = aesUtil.decryptToString(associatedData, nonce, ciphertext);
166+
} catch (GeneralSecurityException e) {
167+
throw new ParseException("AES解密失败,resource:" + resource.toString(), e);
168+
}
169+
notification.setDecryptData(decryptData);
170+
}
171+
172+
}

0 commit comments

Comments
 (0)