Skip to content

Commit

Permalink
增加微信支付通知处理器 (#105)
Browse files Browse the repository at this point in the history
  • Loading branch information
lianup authored Jan 23, 2022
1 parent 9436ad9 commit c5d89c7
Show file tree
Hide file tree
Showing 8 changed files with 512 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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 + '\'' +
'}';
}
}

}
Original file line number Diff line number Diff line change
@@ -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);
}

}
Loading

0 comments on commit c5d89c7

Please sign in to comment.