微信支付之退款

时间:2022-10-25 11:20:48

  微信支付开发完之后,客户提出新要求,要求有退款功能,好吧,完整的支付流程也包括退款,干吧。o_o .... 

  当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,微信支付将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。微信退款功能对应退款申请接口,关于接口注意事项详见申请退款

1. 准备步骤

  由于我是在服务商模式下开发的,服务商模式下,退款接口需要单独申请权限,详见特约商户退款权限授权及解除操作方法

  退款请求需要双向证书, 详见安全规范

  以上是准备步骤,按照文档一步一步来,应该没问题的。

2. 退款申请

  此处直接上代码:

     //微信签名需要的参数
        Map<String, String> signMap = new HashMap<String, String>();
        signMap.put("appid", "");                    // 微信分配的公众账号ID
        signMap.put("mch_id","");                    // 微信支付分配的商户号
        signMap.put("sub_mch_id","");              // 微信支付分配的子商户号
        signMap.put("nonce_str", "");            // 随机数
        signMap.put("transaction_id","");         // 微信生成的订单号,在支付通知中有返回

        signMap.put("out_trade_no","");               // 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母
        signMap.put("out_refund_no","");              // 商户系统内部的退款单号,商户系统内部唯一,同一退款单号多次请求只退一笔
        signMap.put("refund_fee","1");                // 退款总金额,单位为分,只能为整数,可部分退款
        signMap.put("total_fee","1");                 // 订单总金额,单位为分,只能为整数
        signMap.put("notify_url","");                 // 异步接收微信支付退款结果通知的回调地址,通知URL必须为外网可访问的url,不允许带参数如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效
        signMap.put("sign","");                 // 签名
        String xml = WxPayUtil.getRequestXml(signMap); // 生成xml,微信要求的xml形式
        StringBuilder sb2 = new StringBuilder();
        /**
         * JAVA使用证书文件
         */
        logger.info("加载证书开始=========================================》》》》》");
        // 指定读取证书格式为PKCS12
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        // 读取本机存放的PKCS12证书文件
        FileInputStream instream = new FileInputStream(new File("path"));
        try {
            // 指定PKCS12的密码(商户ID)
            keyStore.load(instream, MCH_ID.toCharArray());
        } finally {
            instream.close();
        }
        //ssl双向验证发送http请求报文
        SSLContext sslcontext = null;
        sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, MCH_ID.toCharArray()).build();
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, new String[]{"TLSv1"}, null, SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
        CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
        HttpPost httppost = new HttpPost("https://api.mch.weixin.qq.com/secapi/pay/refund");
        StringEntity se = new StringEntity(xml.toString(), "UTF-8");
        httppost.setEntity(se);
        //定义响应实例对象
        CloseableHttpResponse responseEntry = null;
        String xmlStr2 = null;                  // 读入响应流中字符串的引用
        responseEntry = httpclient.execute(httppost);    // 发送请求
        HttpEntity entity = responseEntry.getEntity();   // 获得响应实例对象
        if (entity != null) {                  // 读取响应流的内容
            BufferedReader bufferedReader = null;
            bufferedReader = new BufferedReader(new InputStreamReader(entity.getContent(), "UTF-8"));
            while ((xmlStr2 = bufferedReader.readLine()) != null) {
                sb2.append(xmlStr2);
            }
        }
        Map<String, String> map = WxPayUtil.doXMLParse(sb2.toString());
        logger.info("申请退款接口返回的结果集======>" + map);
        //return_code为微信返回的状态码,SUCCESS表示申请退款成功,return_msg 如非空,为错误原因 签名失败 参数格式校验错误
        if (map.get("return_code").toString().equalsIgnoreCase("SUCCESS")
                && map.get("result_code").toString().equalsIgnoreCase("SUCCESS")) {
            logger.info("****************退款申请成功!**********************");
       // 修改订单状态为申请退款
       // 业务逻辑
        } else {
            logger.info("*****************退款申请失败!*********************");
       // 失败处理
        }

  这里主要是准备微信要求的参数,加载证书,发送退款请求。其中加载证书是加载本地证书,关于获取本地证书二进制流说明一下:

  Java中读取本地文件可以通过下面的代码完成,其中path代表文件在当前电脑上的路径。

FileInputStream instream = new FileInputStream(new File("path"));

  可以将证书文件置于项目的classpath下,通过获取classpath加上证书文件名(全名,带后缀)来定位证书文件,如何获取classpath,Java工程下主要有以下几种方式:

// 获取classpath的绝对路径
this.getClass().getResource("/").getPath()

// 通过线程方式获取classPath的绝对路径
Thread.currentThread().getContextClassLoader().getResource("").getPath()

// 通过ClassLoader的静态方法获取classpath的绝对路径
ClassLoader.getSystemResource("").getPath()

  通过以上方式获取到的是classpath的绝对路径,这里是动态获取的,好处就是可移植。如果你的证书文件在项目classpath下的子目录中,那就需要使用到路径分隔符,这里最好使用

File.separator,因为在Windows下的路径分隔符和Linux下的路径分隔符是不一样的,如果考虑跨平台,最好是像下面这样写:

File myFile = new File("C:" + File.separator + "tmp" + File.separator, "test.txt");

3. 退款结果通知

  这一部分是微信主动发起的异步回调通知,回调地址在前面发起退款申请时可以指定,商户端需要提供接口接收微信通知,这里需要注意的是退款结果对重要的数据进行了加密,商户需要用商户秘钥进行解密后才能获得结果通知的内容。

  微信官方给出的解密步骤如下:

  1. 对加密串A做base64解码,得到加密串B
  2. 对商户key做md5,得到32位小写key* ( key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置 )
  3. 用key*对加密串B做AES-256-ECB解密(PKCS7Padding)

  主要代码如下,包括两个工具类Base64Util和MD5Util:

public class AESUtil {
    /**
     * 密钥算法
     */
    private static final String ALGORITHM = "AES";
    /**
     * 加解密算法/工作模式/填充方式
     */
    private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS7Padding";
    /**
     * 生成key
     */
    //微信支付API密钥设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置
    private static String paySign = "";
    //对商户key做md5,得到32位小写key*
    private static SecretKeySpec key = new SecretKeySpec(MD5Util.MD5Encode(paySign, "UTF-8").toLowerCase().getBytes(), ALGORITHM);

    /**
     * AES加密
     * @param data
     * @return
     * @throws Exception
     */
    public static String encryptData(String data) throws Exception {
        Security.addProvider(new BouncyCastleProvider());
        // 创建密码器
        Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING, "BC");
        // 初始化
        cipher.init(Cipher.ENCRYPT_MODE, key);
        return Base64Util.encode(cipher.doFinal(data.getBytes()));
    }

    /**
     * AES解密
     *(1)对加密串A做base64解码,得到加密串B
     *(2)用key*对加密串B做AES-256-ECB解密(PKCS7Padding)
     * @param base64Data
     * @return
     * @throws Exception
     */
    public static String decryptData(String base64Data) throws Exception {
        Security.addProvider(new BouncyCastleProvider());
        Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING, "BC");
        cipher.init(Cipher.DECRYPT_MODE, key);
        return new String(cipher.doFinal(Base64Util.decode(base64Data)));
    }

    public static void main(String[] args) throws Exception {
        String A = "微信返回的req_info";
        System.out.println(AESUtil.decryptData(A));
    }
}

class MD5Util{
    private static String byteArrayToHexString(byte b[]) {
        StringBuffer resultSb = new StringBuffer();
        for (int i = 0; i < b.length; i++)
            resultSb.append(byteToHexString(b[i]));

        return resultSb.toString();
    }

    private static String byteToHexString(byte b) {
        int n = b;
        if (n < 0)
            n += 256;
        int d1 = n / 16;
        int d2 = n % 16;
        return hexDigits[d1] + hexDigits[d2];
    }

    public static String MD5Encode(String origin, String charsetname) {
        String resultString = null;
        try {
            resultString = new String(origin);
            MessageDigest md = MessageDigest.getInstance("MD5");
            if (charsetname == null || "".equals(charsetname))
                resultString = byteArrayToHexString(md.digest(resultString
                        .getBytes()));
            else
                resultString = byteArrayToHexString(md.digest(resultString
                        .getBytes(charsetname)));
        } catch (Exception exception) {
        }
        return resultString;
    }

    private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
}

class Base64Util{
    private static final char S_BASE64CHAR[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
            'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
            'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'};
    private static final byte S_DECODETABLE[];

    static {
        S_DECODETABLE = new byte[128];
        for (int i = 0; i < S_DECODETABLE.length; i++)
            S_DECODETABLE[i] = 127;

        for (int i = 0; i < S_BASE64CHAR.length; i++)
            S_DECODETABLE[S_BASE64CHAR[i]] = (byte) i;

    }

    private static int decode0(char ibuf[], byte obuf[], int wp) {
        int outlen = 3;
        if (ibuf[3] == '=')
            outlen = 2;
        if (ibuf[2] == '=')
            outlen = 1;
        int b0 = S_DECODETABLE[ibuf[0]];
        int b1 = S_DECODETABLE[ibuf[1]];
        int b2 = S_DECODETABLE[ibuf[2]];
        int b3 = S_DECODETABLE[ibuf[3]];
        switch (outlen) {
            case 1: // '\001'
                obuf[wp] = (byte) (b0 << 2 & 252 | b1 >> 4 & 3);
                return 1;

            case 2: // '\002'
                obuf[wp++] = (byte) (b0 << 2 & 252 | b1 >> 4 & 3);
                obuf[wp] = (byte) (b1 << 4 & 240 | b2 >> 2 & 15);
                return 2;

            case 3: // '\003'
                obuf[wp++] = (byte) (b0 << 2 & 252 | b1 >> 4 & 3);
                obuf[wp++] = (byte) (b1 << 4 & 240 | b2 >> 2 & 15);
                obuf[wp] = (byte) (b2 << 6 & 192 | b3 & 63);
                return 3;
        }
        throw new RuntimeException("Internal error");
    }

    public static byte[] decode(char data[], int off, int len) {
        char ibuf[] = new char[4];
        int ibufcount = 0;
        byte obuf[] = new byte[(len / 4) * 3 + 3];
        int obufcount = 0;
        for (int i = off; i < off + len; i++) {
            char ch = data[i];
            if (ch != '=' && (ch >= S_DECODETABLE.length || S_DECODETABLE[ch] == 127))
                continue;
            ibuf[ibufcount++] = ch;
            if (ibufcount == ibuf.length) {
                ibufcount = 0;
                obufcount += decode0(ibuf, obuf, obufcount);
            }
        }

        if (obufcount == obuf.length) {
            return obuf;
        } else {
            byte ret[] = new byte[obufcount];
            System.arraycopy(obuf, 0, ret, 0, obufcount);
            return ret;
        }
    }

    public static byte[] decode(String data) {
        char ibuf[] = new char[4];
        int ibufcount = 0;
        byte obuf[] = new byte[(data.length() / 4) * 3 + 3];
        int obufcount = 0;
        for (int i = 0; i < data.length(); i++) {
            char ch = data.charAt(i);
            if (ch != '=' && (ch >= S_DECODETABLE.length || S_DECODETABLE[ch] == 127))
                continue;
            ibuf[ibufcount++] = ch;
            if (ibufcount == ibuf.length) {
                ibufcount = 0;
                obufcount += decode0(ibuf, obuf, obufcount);
            }
        }

        if (obufcount == obuf.length) {
            return obuf;
        } else {
            byte ret[] = new byte[obufcount];
            System.arraycopy(obuf, 0, ret, 0, obufcount);
            return ret;
        }
    }

    public static void decode(char data[], int off, int len, OutputStream ostream) throws IOException {
        char ibuf[] = new char[4];
        int ibufcount = 0;
        byte obuf[] = new byte[3];
        for (int i = off; i < off + len; i++) {
            char ch = data[i];
            if (ch != '=' && (ch >= S_DECODETABLE.length || S_DECODETABLE[ch] == 127))
                continue;
            ibuf[ibufcount++] = ch;
            if (ibufcount == ibuf.length) {
                ibufcount = 0;
                int obufcount = decode0(ibuf, obuf, 0);
                ostream.write(obuf, 0, obufcount);
            }
        }

    }

    public static void decode(String data, OutputStream ostream) throws IOException {
        char ibuf[] = new char[4];
        int ibufcount = 0;
        byte obuf[] = new byte[3];
        for (int i = 0; i < data.length(); i++) {
            char ch = data.charAt(i);
            if (ch != '=' && (ch >= S_DECODETABLE.length || S_DECODETABLE[ch] == 127))
                continue;
            ibuf[ibufcount++] = ch;
            if (ibufcount == ibuf.length) {
                ibufcount = 0;
                int obufcount = decode0(ibuf, obuf, 0);
                ostream.write(obuf, 0, obufcount);
            }
        }

    }

    public static String encode(byte data[]) {
        return encode(data, 0, data.length);
    }

    public static String encode(byte data[], int off, int len) {
        if (len <= 0)
            return "";
        char out[] = new char[(len / 3) * 4 + 4];
        int rindex = off;
        int windex = 0;
        int rest;
        for (rest = len - off; rest >= 3; rest -= 3) {
            int i = ((data[rindex] & 255) << 16) + ((data[rindex + 1] & 255) << 8) + (data[rindex + 2] & 255);
            out[windex++] = S_BASE64CHAR[i >> 18];
            out[windex++] = S_BASE64CHAR[i >> 12 & 63];
            out[windex++] = S_BASE64CHAR[i >> 6 & 63];
            out[windex++] = S_BASE64CHAR[i & 63];
            rindex += 3;
        }

        if (rest == 1) {
            int i = data[rindex] & 255;
            out[windex++] = S_BASE64CHAR[i >> 2];
            out[windex++] = S_BASE64CHAR[i << 4 & 63];
            out[windex++] = '=';
            out[windex++] = '=';
        } else if (rest == 2) {
            int i = ((data[rindex] & 255) << 8) + (data[rindex + 1] & 255);
            out[windex++] = S_BASE64CHAR[i >> 10];
            out[windex++] = S_BASE64CHAR[i >> 4 & 63];
            out[windex++] = S_BASE64CHAR[i << 2 & 63];
            out[windex++] = '=';
        }
        return new String(out, 0, windex);
    }

    public static void encode(byte data[], int off, int len, OutputStream ostream) throws IOException {
        if (len <= 0)
            return;
        byte out[] = new byte[4];
        int rindex = off;
        int rest;
        for (rest = len - off; rest >= 3; rest -= 3) {
            int i = ((data[rindex] & 255) << 16) + ((data[rindex + 1] & 255) << 8) + (data[rindex + 2] & 255);
            out[0] = (byte) S_BASE64CHAR[i >> 18];
            out[1] = (byte) S_BASE64CHAR[i >> 12 & 63];
            out[2] = (byte) S_BASE64CHAR[i >> 6 & 63];
            out[3] = (byte) S_BASE64CHAR[i & 63];
            ostream.write(out, 0, 4);
            rindex += 3;
        }

        if (rest == 1) {
            int i = data[rindex] & 255;
            out[0] = (byte) S_BASE64CHAR[i >> 2];
            out[1] = (byte) S_BASE64CHAR[i << 4 & 63];
            out[2] = 61;
            out[3] = 61;
            ostream.write(out, 0, 4);
        } else if (rest == 2) {
            int i = ((data[rindex] & 255) << 8) + (data[rindex + 1] & 255);
            out[0] = (byte) S_BASE64CHAR[i >> 10];
            out[1] = (byte) S_BASE64CHAR[i >> 4 & 63];
            out[2] = (byte) S_BASE64CHAR[i << 2 & 63];
            out[3] = 61;
            ostream.write(out, 0, 4);
        }
    }

    public static void encode(byte data[], int off, int len, Writer writer) throws IOException {
        if (len <= 0)
            return;
        char out[] = new char[4];
        int rindex = off;
        int rest = len - off;
        int output = 0;
        do {
            if (rest < 3)
                break;
            int i = ((data[rindex] & 255) << 16) + ((data[rindex + 1] & 255) << 8) + (data[rindex + 2] & 255);
            out[0] = S_BASE64CHAR[i >> 18];
            out[1] = S_BASE64CHAR[i >> 12 & 63];
            out[2] = S_BASE64CHAR[i >> 6 & 63];
            out[3] = S_BASE64CHAR[i & 63];
            writer.write(out, 0, 4);
            rindex += 3;
            rest -= 3;
            if ((output += 4) % 76 == 0)
                writer.write("\n");
        }
        while (true);
        if (rest == 1) {
            int i = data[rindex] & 255;
            out[0] = S_BASE64CHAR[i >> 2];
            out[1] = S_BASE64CHAR[i << 4 & 63];
            out[2] = '=';
            out[3] = '=';
            writer.write(out, 0, 4);
        } else if (rest == 2) {
            int i = ((data[rindex] & 255) << 8) + (data[rindex + 1] & 255);
            out[0] = S_BASE64CHAR[i >> 10];
            out[1] = S_BASE64CHAR[i >> 4 & 63];
            out[2] = S_BASE64CHAR[i << 2 & 63];
            out[3] = '=';
            writer.write(out, 0, 4);
        }
    }
}

  需要添加的依赖:

<dependency>  
    <groupId>org.bouncycastle</groupId>  
    <artifactId>bcprov-jdk15on</artifactId>  
    <version>1.47</version>  
</dependency>

4. 遇到的问题

解密时报java.security.InvalidKeyException: Illegal key size

原因:Illegal key size or default parameters 是指密钥长度受限制,java运行时环境读到的是受限的policy文件,policy文件位于${java_home}/jre/lib/security 目录下,这种限制是因为美国对软件出口的控制。

解决

  • 在官方网站下载JCE无限制权限策略文件,下载地址:JDK6JDK7JDK8
  • 下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt;
  • 如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件;
  • 如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件;

5. 总结

  总的来说微信支付退款申请接口比较简单,参考了一些博客之后,顺利的测通了,效果如下:

微信支付之退款

6. 参考文献

申请退款

特约商户退款权限授权及解除操作方法

2018.05.24 解密微信退款结果通知中的加密信息req_info

Java实现AES加密,异常java.security.InvalidKeyException: Illegal key size 的解决

微信公众号授权,支付,退款总结