现象
使用Java解密时,抛出异常AEADBadTagException: Tag mismatch!
错误信息
Tag mismatch
微信官方说明
官网说明
解决过程
疯狂之路
- 微信社区,参考了多种方案 微信社区-问题反馈参考
- 度娘搜索,getBytes时传入编码“UTF-8”
- 检查 apiv3秘钥在平台设置的和程序中使用的是否一致
- 检查是否Base64解码,其实使用微信官方的程序即可,官方程序-java版-解密
走上正轨
- 微信客服沟通(重要解决手段,也是最终查出问题所在的关键)
- 客服说回调数据中,普通支付,associated_data参数一定会传过来,但是在官网对于v3支付结果回调说明中,提示associated_data不是必传的,而且我也没有找到相关说明在什么情况下传,什么情况不传,有找到的伙伴可以留言做个说明
- 第6条中的问题,让我相信微信发送和我接收的数据没有问题😢 ,其实,最主要的就是缺少了associated_data数据,造成解密时出的问题
发现问题
- 接口使用的@RequestBody接收微信发送过来的数据,但是,但是,但是,接收的数据丢失了一部分,😱😱😱,这个不清楚是springboot框架的问题,还是微信推送消息机制的问题,最终使用HttpServletRequest读取stream的方式解决
代码-测试正常解密
controller
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
/**
* @author panda
* @date 2021/4/27 14:06
* <p>
* description 回调接口
*/
@RestController
@RequestMapping("/pay")
public class PortalWechatController {
@Autowired
private PortalWechatService portalWechatService;
@PostMapping("/wx/callback")
public String wxCallback(HttpServletRequest request){
return portalWechatService.callBack(request);
}
}
service
import javax.servlet.http.HttpServletRequest;
/**
* @author panda
* @date 2021/4/27 14:10
* <p>
* description service
*/
public interface PortalWechatService {
/**
* 微信支付回调
* @param request 请求
* @return 结果
*/
String callBack(HttpServletRequest request);
}
serviceImpl
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
/**
* @author panda
* @date 2021/4/27 14:11
* <p>
* description serviceImpl
*/
@Slf4j
@Service
public class PortalWechatServiceimpl implements PortalWechatService {
/**
* 微信支付回调
*
* @param request 请求
* @return 结果
*/
@Override
public String callBack(HttpServletRequest request) {
// TODO 必须使用request获取body(readLine读取),微信推送的消息使用@RequestBody可能一次性无法读完,造成解密失败
String resCode = "SUCCESS";
String resMessage = "成功";
String streamReadString = getRequestBody(request);
WxPayCallbackModel model = JSONObject.parseObject(streamReadString, WxPayCallbackModel.class);
log.info("pay callback: request read={}", streamReadString);
try {
WxPayCallbackResourceModel resource = model.getResource();
String associatedData = resource.getAssociated_data();
String nonce = resource.getNonce();
String ciphertext = resource.getCiphertext();
byte[] aesKey = WeChatPayCostant.API_V3_SECRET.trim().toLowerCase().getBytes("UTF-8");
byte[] associatedDataBytes = associatedData.getBytes("UTF-8");
byte[] nonceBytes = nonce.getBytes("UTF-8");
byte[] ciphertextBytes = Base64.decodeBase64(ciphertext);
// 开始解密
AesUtil aesUtil = new AesUtil(aesKey);
String decryptedString = aesUtil.decryptToString(associatedDataBytes, nonceBytes, ciphertextBytes);
log.info("微信支付回调 - 解密: {}", decryptedString);
// 解密得到的json结果
JSONObject decryptedJsonObj = JSONObject.parseObject(decryptedString);
String code = decryptedJsonObj.getString("out_trade_no");
// 1. 数据库通过订单号查询到的的订单
// 2. 修改订单中支付状态,微信通知状态,支付相关其它信息
} catch (Exception e) {
log.error("支付回调失败: {}", e.getMessage());
// e.printStackTrace();
resCode = "FAIL";
resMessage = "支付失败";
}
JSONObject returnJson = new JSONObject();
returnJson.put("code", resCode);
returnJson.put("message", resMessage);
return returnJson.toJSONString();
}
// *****************************************************业务支撑*****************************************************
/**
* 获取请求体
* @param request 请求
*/
private String getRequestBody(HttpServletRequest request) {
ServletInputStream stream = null;
BufferedReader reader = null;
StringBuilder sb = new StringBuilder();
try {
stream = request.getInputStream();
// 获取响应
reader = new BufferedReader(new InputStreamReader(stream));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
assert stream != null;
try {
stream.close();
} catch (IOException e) {
e.printStackTrace();
}
assert reader != null;
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return sb.toString();
}
}
相关工具和实体
import lombok.Data;
/**
* @author panda
* @date 2021/5/8 14:12
* <p>
* description 微信支付回调 model
*/
@Data
public class WxPayCallbackModel {
/**
* 通知的唯一ID
* 示例值:EV-2018022511223320873
*/
private String id = "";
/**
* 通知创建的时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE,YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss.表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示北京时间2015年05月20日13点29分35秒。
* 示例值:2015-05-20T13:29:35+08:00
*/
private String create_time = "";
/**
* 通知的类型,支付成功通知的类型为TRANSACTION.SUCCESS
* 示例值:TRANSACTION.SUCCESS
*/
private String event_type = "";
/**
* 通知的资源数据类型,支付成功通知为encrypt-resource
* 示例值:encrypt-resource
*/
private String resource_type = "";
/**
* 回调摘要
* 示例值:支付成功
*/
private String summary = "";
/**
* 通知资源数据
* json格式,见示例
*/
private WxPayCallbackResourceModel resource;
@Override
public String toString() {
return "WxPayCallbackModel{" +
"id='" + id + '\'' +
", create_time='" + create_time + '\'' +
", event_type='" + event_type + '\'' +
", resource_type='" + resource_type + '\'' +
", summary='" + summary + '\'' +
", resource=" + resource.toString() +
'}';
}
}
import lombok.Data;
/**
* @author panda
* @date 2021/5/8 14:12
* <p>
* description 微信支付回调通知数据 model
*/
@Data
public class WxPayCallbackResourceModel {
/**
* 加密算法类型
* 对开启结果数据进行加密的加密算法,目前只支持AEAD_AES_256_GCM
* 示例值:AEAD_AES_256_GCM
*/
private String algorithm = "";
/**
* 数据密文
* Base64编码后的开启/停用结果数据密文
* 示例值:sadsadsadsad
*/
private String ciphertext = "";
/**
* 附加数据
* 附加数据
* 示例值:fdasfwqewlkja484w
*/
private String associated_data = "";
/**
* 原始类型
* 原始回调类型,为transaction
* 示例值:transaction
*/
private String original_type = "";
/**
* 随机串
* 加密使用的随机串
* 示例值:fdasflkja484w
*/
private String nonce = "";
@Override
public String toString() {
return "WxPayCallbackResourceModel{" +
"algorithm='" + algorithm + '\'' +
", ciphertext='" + ciphertext + '\'' +
", associated_data='" + associated_data + '\'' +
", original_type='" + original_type + '\'' +
", nonce='" + nonce + '\'' +
'}';
}
}
/**
* @author panda
* @date 2021/4/27 11:21
* <p>
* description 微信支付常量
*/
public interface WeChatPayCostant {
/**
* 应用id
*/
String APP_ID = "";
/**
* 直连商户号
*/
String MCH_ID = "";
/**
* 通知地址
*/
String NOTIFY_URL = "https://a.b.c.com/xxx";
/**
* 货币类型
*/
String CURRENCY = "CNY";
/**
* 微信支付url
*/
String PAY_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
/**
* APIv3密钥 32位(数字 + 字母_小写),可以直接使用Hutool的IdUtil.simpleUuid()生成
*/
String API_V3_SECRET = "";
/**
* 商户API证书序列号
*/
String SERIAL_NO = "";
}
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
public class AesUtil {
static final int KEY_LENGTH_BYTE = 32;
static final int TAG_LENGTH_BIT = 128;
private final byte[] aesKey;
public AesUtil(byte[] key) {
if (key.length != 32) {
throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
} else {
this.aesKey = key;
}
}
public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext) throws GeneralSecurityException, IOException {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec key = new SecretKeySpec(this.aesKey, "AES");
GCMParameterSpec spec = new GCMParameterSpec(128, nonce);
cipher.init(2, key, spec);
cipher.updateAAD(associatedData);
return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "UTF-8");
} catch (NoSuchPaddingException | NoSuchAlgorithmException var7) {
throw new IllegalStateException(var7);
} catch (InvalidAlgorithmParameterException | InvalidKeyException var8) {
throw new IllegalArgumentException(var8);
}
}
public String decryptToString(byte[] associatedData, byte[] nonce, byte[] ciphertext) throws GeneralSecurityException, IOException {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec key = new SecretKeySpec(this.aesKey, "AES");
GCMParameterSpec spec = new GCMParameterSpec(128, nonce);
cipher.init(2, key, spec);
cipher.updateAAD(associatedData);
return new String(cipher.doFinal(ciphertext), "UTF-8");
} catch (NoSuchPaddingException | NoSuchAlgorithmException var7) {
throw new IllegalStateException(var7);
} catch (InvalidAlgorithmParameterException | InvalidKeyException var8) {
throw new IllegalArgumentException(var8);
}
}
}