前言
前面一篇说了微信公众号支付,趁着手热,顺便把退款也测试了,强调一下,这就是搭建环境测试流程的,没有对接业务,所以有些地方怎么方便怎么来的,退款要讲的就是安装P12证书,以及退款回调 req_info的解密
环境准备
微信商户平台–>账户设置–> API安全–> 点击申请,按要求即可,下载完之后,解压之后如下图几个文件
windows上可以直接双击导入系统,导入过程中会提示输入证书密码,证书密码默认为您的商户号,导入成功即可,Linux系统如果是Java版的好像直接匹配P12证书路径就可以了,没有测试
退款代码
工具类可以看我上一篇公众号支付,这里就不多讲,直接撸
controller层代码
/**
* 退款
* @param tradeNo 退款的原交易订单号
* @param refundFee 退款金额
* @param desc 退款说明
* @return
*/
@PostMapping("/refund")
@ResponseBody
public ResultEntity exRefund(@RequestParam("tradeNo")String tradeNo,@RequestParam("refundFee")Integer refundFee,
@RequestParam(value = "desc",required = false)String desc){
if(StringUtils.isBlank(tradeNo) || null == refundFee){
return ResultEntity.fail("交易订单号和退款金额必传");
}
if(!wxPayService.exRefund(tradeNo, refundFee, desc)){
return ResultEntity.fail("退款失败");
}
return ResultEntity.success();
}
实现类代码
官方接口文档
我的订单总额是写死的,可以根据订单号去查数据库的
@Override
public boolean exRefund(String tradeNo, Integer refundFee, String desc) {
Map<String, String> map = new HashMap<>();
map.put("appid", wechatAccountConfig.getAppId());
map.put("mch_id", wechatAccountConfig.getMchId());
map.put("nonce_str", WXPayUtil.generateNonceStr());
map.put("out_trade_no", tradeNo);
String refundNo = System.currentTimeMillis()+"";
map.put("out_refund_no", refundNo);
map.put("total_fee", "5");
map.put("refund_fee", refundFee.toString());
if(StringUtils.isNotBlank(desc)){
map.put("refund_desc", desc);
}
map.put("notify_url",wechatAccountConfig.getRefundNotifyUrl());
try {
String sign = WXPayUtil.generateSignature(map, wechatAccountConfig.getMchKey());
map.put("sign",sign);
} catch (Exception e) {
e.printStackTrace();
log.error("退款申请签名错误");
return false;
}
log.info("退款申请请求参数:{}",JSON.toJSONString(map));
String mapToXml;
try {
mapToXml = WXPayUtil.mapToXml(map);
} catch (Exception e) {
log.error("申請退款map转str出错");
e.printStackTrace();
return false;
}
try {
String response = HttpUtil.doRefund(Constants.WXPAY_REFUND_GATEWAY, mapToXml, wechatAccountConfig.getMchId(),wechatAccountConfig.getCertPath());
Map<String, String> responseMap = WXPayUtil.xmlToMap(response);
log.info("退款申请返回结果:{}",JSON.toJSONString(responseMap));
if (responseMap.get("return_code").equals(Constants.SUCCESS)&& responseMap.get("result_code").equals(Constants.SUCCESS)) {
log.info("退款成功");
}else {
log.info("退款失败");
return false;
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
这里涉及到 doRefund 这个方法,这里是直接百度找的
public static String doRefund(String url,String data,String mchId,String certPath) throws Exception {
//指定读取证书格式为PKCS12(注意PKCS12证书 是从微信商户平台-》账户设置-》 API安全 中下载的)
KeyStore keyStore = KeyStore.getInstance("PKCS12");
//读取本机存放的PKCS12证书文件 certPath 就是我本地存放的路径
FileInputStream instream = new FileInputStream(new File(certPath));
//比如安装在D:/pkcs12/apiclient_cert.p12情况下,就可以写成如下语句
//FileInputStream instream = new FileInputStream(new File("D:/pkcs12/apiclient_cert.p12"));
try {
//指定PKCS12的密码(商户ID)
keyStore.load(instream, mchId.toCharArray());
} finally {
instream.close();
}
SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, mchId.toCharArray()).build();
//指定TLS版本
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory( sslcontext,new String[] { "TLSv1"},null,SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
//设置httpclient的SSLSocketFactory
CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
try {
HttpPost httpost = new HttpPost(url);
httpost.setEntity(new StringEntity(data, "UTF-8"));
CloseableHttpResponse response = httpclient.execute(httpost);
try {
HttpEntity entity = response.getEntity();
String jsonStr = EntityUtils.toString(response.getEntity(), "UTF-8");
EntityUtils.consume(entity);
return jsonStr;
} finally {
response.close();
}
} finally {
httpclient.close();
}
}
退款回调controller方法
@RequestMapping("/refundNotify")
public void refundNotify(HttpServletRequest request, HttpServletResponse response){
if(wxPayService.refundNotify(request,response)){
try {
// 微信响应消息
response.getWriter().write(Constants.NOTIFY_RESPONSE_BODY);
} catch (IOException e) {
e.printStackTrace();
}
}else {
log.error("退款回调出现异常");
}
}
退款回调实现类
@Override
public boolean refundNotify(HttpServletRequest request, HttpServletResponse response) {
InputStream is = null;
try {
is = request.getInputStream();//获取请求的流信息(这里是微信发的xml格式所有只能使用流来读)
String xml = WXPayUtil.inputStream2String(is, "UTF-8");
Map<String, String> notifyMap = WXPayUtil.xmlToMap(xml);//将微信发的xml转map
log.info("退款回调返回的数据:{}", JSON.toJSONString(notifyMap));
// 验签返回的数据
if(notifyMap.get("return_code").equals(Constants.SUCCESS)){
// 此时在解密 req_info 字段信息
Map<String, String> reqInfo = WXPayUtil.xmlToMap(AesUtil.decryptData(notifyMap.get("req_info"),wechatAccountConfig.getMchKey()));
if(!reqInfo.get("refund_status").equals(Constants.SUCCESS)){
return false;
}
}else {
log.info("退款失败:{}",JSON.toJSONString(notifyMap));
return false;
}
} catch (Exception e) {
e.printStackTrace();
return false;
}finally {
if(null != is){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return true;
}
上面注意的是req_info解析
先上解密类的代码,工具类百度找的
public class AesUtil {
/**
* 密钥算法
*/
private static final String ALGORITHM = "AES";
/**
* 加解密算法/工作模式/填充方式
*/
private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS7Padding";
/**
* AES加密
*
* @param data d
* @return str
* @throws Exception e
*/
public static String encryptData(String data) throws Exception {
// 创建密码器
Security.addProvider(new BouncyCastleProvider());
Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING, "BC");
SecretKeySpec key = new SecretKeySpec(WXPayUtil.MD5("T0rai838GUHYkTNYWWGoxzJLAOE7HUa1").toLowerCase().getBytes(), ALGORITHM);
// 初始化
cipher.init(Cipher.ENCRYPT_MODE, key);
return base64Encode8859(new String(cipher.doFinal(data.getBytes()), "ISO-8859-1"));
}
/**
* AES解密
*
* @param base64Data 64
* @return str
* @throws Exception e
*/
public static String decryptData(String base64Data,String mchKey) throws Exception {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING, "BC");
SecretKeySpec key = new SecretKeySpec(WXPayUtil.MD5(mchKey).toLowerCase().getBytes(), ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, key);
return new String(cipher.doFinal(base64Decode8859(base64Data).getBytes("ISO-8859-1")), "utf-8");
}
/**
* Base64解码
* @param source base64 str
* @return str
*/
public static String base64Decode8859(final String source) {
String result = "";
final Base64.Decoder decoder = Base64.getDecoder();
try {
// 此处的字符集是ISO-8859-1
result = new String(decoder.decode(source), "ISO-8859-1");
} catch (final UnsupportedEncodingException e) {
e.printStackTrace();
}
return result;
}
/**
* Base64加密
* @param source str
* @return base64 str
*/
public static String base64Encode8859(final String source) {
String result = "";
final Base64.Encoder encoder = Base64.getEncoder();
byte[] textByte = null;
try {
//注意此处的编码是ISO-8859-1
textByte = source.getBytes("ISO-8859-1");
result = encoder.encodeToString(textByte);
} catch (final UnsupportedEncodingException e) {
e.printStackTrace();
}
return result;
}
}
如果解密代码报 java.security.InvalidKeyException: Illegal key size or default parameters这个错
,则看下面
原因
因为美国的进口管制限制,Java发布的运行环境包中的加解密有一定的限制,默认不允许256位密钥的AES加解密
解决办法
1、如果是 Java 1.8.0_151以前的版本,则下载 https://www.oracle.com/java/technologies/javase-jce8-downloads.html,解压之后可以看到 local_policy.jar和US_export_policy.jar 这个2个文件,替换掉你本地jdk目录/jre/lib/security/ 下面的local_policy.jar和US_export_policy.jar即可,此方法是亲测,如果你没有效果,那就在把 jre本地目录/lib/security,也替换掉这2个文件在试一下
2、如果是 Java 1.8.0_151 以后的版本
则在 本地jdk目录/jre/lib/security 有一个 java.security 文件打开,找到定义java安全性属性 crypto.policy的行,他有limited或unlimited - 默认值是limited。默认情况是 #crypto.policy=unlimited
,打开注释,去掉#即可,大概在 860 多行
题外话
原创
这里顺便说一下,linux上如果没有配置环境变量的情况下,一般是命令安装Javajdk的,怎么找这些 java.security 文件
1、echo $JAVA_HOME 先确定下是否配置了环境变量,没有的话,输出的是空
2、which java 查看Java目录
3、第2步之后不出意外会输出 /usr/bin/java ,接下来在使用 ls -lrt 命令
-a :显示所有文件即目录(ls内定将文件名或目录名称开头为“.”的视为隐藏档,不会列出)
-l: 除文件名称外,亦将文件形态、权限、拥有者、文件大小等资讯详细列出。
-r: 将文件以相反次序显示(原定依英文字母次序)。
-t: 将文件依次建立时间之先后次序列出。
-A: 同-a,但不列出“.” (当前目录)及“…”(副文件)。
-F: 在列出的文件名称后加一符号;例如可执行档则加“*”,目录则加“/”
-R: 若目录下有文件,则以下之文件亦皆依序里列出。
4、执行 ls -lrt /usr/bin/java 会弹出一个目录,在继续 ls -lrt 命令查询即可找到