记一次微信支付验签始终异常问题,折腾了两宿,最后是 body 有问题 o(╥﹏╥)o
这里使用的是
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.1</version>
</dependency>
噩梦的开始与问题的发现
我最开始的写法:
public Map<String, String> nation(HttpServletRequest request,
@RequestBody PayResult payResult) {
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(payConfig.getTenantAccount().getMerchantSerialNumber())
.nonce(payResult.getResource().getNonce())
.signature(signature)
.timestamp(timestamp)
// 若未设置signType,默认值为 WECHATPAY2-SHA256-RSA2048
.signType(signType)
.body(JSON.toJSONString(payResult))
.build();
}
我这里是使用 RequestBody 接收的参数,因为官方文档说明通知参数类型为 application/json
,如此解析并没有问题,也可以获取到微信发过来的数据
但是问题也是出在这里,o(╥﹏╥)o,无意间搜到了这篇文章https://developers.weixin.qq.com/community/pay/doc/00020ad2448618e57a1b5faf250800
里面提到,微信验签时 请求体 的字符串顺序不能发生任何改变,以退款通知为例,验签时传入的对象键值对顺序必须与为文档保持一致:
{
"id":"EV-2018022511223320873",
"create_time":"2018-06-08T10:34:56+08:00",
"resource_type":"encrypt-resource",
"event_type":"REFUND.SUCCESS",
"summary":"退款成功",
"resource" : {
"original_type": "refund",
"algorithm":"AEAD_AES_256_GCM",
"ciphertext": "...",
"associated_data": "",
"nonce": "..."
}
}
而 @RequestBody
在解析参数时会改变参数顺序,我用 .body(JSON.toJSONString(payResult))
传进去就出了问题
问题的解决
所以最后只能在HTTP的流中获取请求体
@PostMapping("nation")
@NoAuth
public Map<String, String> nation(HttpServletRequest request) throws IOException {
VerdureRequestWrapper requestWrapper = new VerdureRequestWrapper(request);
logger.info("接收微信通知:");
logger.info(requestWrapper.getRequestParams());
String signature = RequestUtils.getHeader(request, "Wechatpay-Signature");
String timestamp = RequestUtils.getHeader(request, "Wechatpay-Timestamp");
String signType = RequestUtils.getHeader(request, "Wechatpay-Signature-Type");
String serial = RequestUtils.getHeader(request, "Wechatpay-Serial");
String nonce = RequestUtils.getHeader(request, "Wechatpay-Nonce");
logger.info("===================================================:");
Enumeration<String> headerNames = request.getHeaderNames();
while(headerNames.hasMoreElements()){
String element = headerNames.nextElement();
System.out.println(element+":"+request.getHeader(element));
}
PayResult payResult = JSON.parseObject(requestWrapper.getRequestParams(), PayResult.class);
// 构造 RequestParam
// 获取HTTP请求头中的 Wechatpay-Signature 、 Wechatpay-Nonce 、 Wechatpay-Timestamp 、 Wechatpay-Serial 、 Request-ID 、Wechatpay-Signature-Type 对应的值,构建 RequestParam 。
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(serial)
.nonce(nonce)
.signature(signature)
.timestamp(timestamp)
// 若未设置signType,默认值为 WECHATPAY2-SHA256-RSA2048
.signType(signType)
.body(requestWrapper.getRequestParams())
.build();
// 初始化 NotificationConfig
NotificationConfig rsaNotificationConfig = new RSANotificationConfig.Builder()
.apiV3Key(this.payConfig.getTenantAccount().getApiV3Key())
.certificates(this.payConfig.getWechatCertificate())
.build();
// 初始化 NotificationParser
NotificationParser parser=new NotificationParser(rsaNotificationConfig);
WePayNation nation = new WePayNation();
nation.setSummary(payResult.getSummary());
nation.setResourceType(payResult.getResource_type());
nation.setRequestData(JSON.toJSONString(payResult.getResource()));
nation.setOriginalType(payResult.getResource().getOriginal_type());
nation.setNationTime(payResult.getCreate_time());
nation.setNationId(payResult.getId());
nation.setEventType(payResult.getEvent_type());
try {
switch (payResult.getEvent_type()) {
case "TRANSACTION.SUCCESS": // 支付通知
// 验签并解密报文
Transaction decryptObject = parser.parse(requestParam, Transaction.class);
nation.setDecodeData(JSON.toJSONString(decryptObject));
// 调用接口处理支付结果
payService.paySuccess(decryptObject);
// 通知业务服务
MinAppHolderReader.app().paySuccess(decryptObject);
break;
case "REFUND.SUCCESS": // 退款成功通知
case "REFUND.ABNORMAL": // 退款异常通知
case "REFUND.CLOSED": // 退款关闭通知
RefundNotification refundNotification = parser.parse(requestParam, RefundNotification.class);
nation.setDecodeData(JSON.toJSONString(refundNotification));
switch (payResult.getEvent_type()) {
case "REFUND.SUCCESS": // 退款成功通知
MinAppHolderReader.app().refundSuccess(refundNotification);
payService.refundSuccess(refundNotification, 8, "退款成功");
break;
case "REFUND.ABNORMAL": // 退款异常通知
MinAppHolderReader.app().refundError(refundNotification);
payService.refundSuccess(refundNotification, 9, "退款异常");
break;
case "REFUND.CLOSED": // 退款关闭通知
MinAppHolderReader.app().refundClose(refundNotification);
payService.refundSuccess(refundNotification, 10, "退款关闭");
break;
}
break;
}
} catch (Exception e) {
e.printStackTrace();
loggerApi.error("微信通知接收解密处理时发生异常", e);
Map<String, String> res = new HashMap<>();
res.put("code", "FAIL");
res.put("message", e.getMessage());
return res;
} finally {
nation.setSystemCreateUser("WX");
payNationMapper.insert(nation);
}
Map<String, String> res = new HashMap<>();
res.put("code", "SUCCESS");
res.put("message", "成功");
return res;
}
我我系统内部因为有重构 httpServletRequest
的方法直接复用了,也可以使用其他方式获取 POST
请求体
若使用自定义方法读取 post
请求体,就不能再使用 @RquestBody
解析参数了,因为 HttpServletRequest
请求体只允许被读取一次!!
public class VerdureRequestWrapper extends HttpServletRequestWrapper {
HttpServletRequest orgRequest = null;
private byte[] bytes;
private WrappedServletInputStream wrappedServletInputStream;
public VerdureRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.orgRequest = request;
//读取输入流的请求参数,保存到bytes中
bytes = IOUtils.toByteArray(request.getInputStream());
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
this.wrappedServletInputStream = new WrappedServletInputStream(byteArrayInputStream);
//把post参数重新写入请求流
reWriteInputStream();
}
public void setRequestParams(String json) {
wrappedServletInputStream.setStream(new ByteArrayInputStream(json.getBytes()));
}
/**
* 把参数重新写进请求里
*/
public void reWriteInputStream() {
wrappedServletInputStream.setStream(new ByteArrayInputStream(bytes != null ? bytes : new byte[0]));
}
@Override
public ServletInputStream getInputStream() throws IOException {
return wrappedServletInputStream;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(wrappedServletInputStream));
}
/**
* 获取post参数
*/
public String getRequestParams() throws IOException {
return new String(bytes, this.getCharacterEncoding());
}
//清洗参数,防止xss注入
public String[] getParameterValues(String parameter) {
String[] values = super.getParameterValues(parameter);
if (values == null) {
return null;
}
int count = values.length;
String[] encodedValues = new String[count];
for (int i = 0; i < count; i++) {
encodedValues[i] = xssEncode(values[i]);
}
return encodedValues;
}
public String getParameter(String name){
String value = super.getParameter(xssEncode(name));
if (value != null) {
value = xssEncode(value);
}
return value;
}
public String getHeader(String name) {
String value = super.getHeader(xssEncode(name));
if (value != null) {
value = xssEncode(value);
}
return value;
}
private static String xssEncode(String s){
return s;
}
public HttpServletRequest getOrgRequest(){
return this.orgRequest;
}
private class WrappedServletInputStream extends ServletInputStream {
public void setStream(InputStream stream) {
this.stream = stream;
}
private InputStream stream;
public WrappedServletInputStream(InputStream stream) {
this.stream = stream;
}
public int read() throws IOException {
return stream.read();
}
public boolean isFinished() {
return true;
}
public boolean isReady() {
return true;
}
public void setReadListener(ReadListener readListener) {
}
}
}