1. 程式人生 > >微信支付之退款

微信支付之退款

ttpClient color url 參考文獻 secret 網站 ces 密鑰 將在

  微信支付開發完之後,客戶提出新要求,要求有退款功能,好吧,完整的支付流程也包括退款,幹吧。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無限制權限策略文件,下載地址:JDK6、JDK7、JDK8;
  • 下載後解壓,可以看到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 的解決

微信公眾號授權,支付,退款總結

微信支付之退款