微信支付之退款
微信支付開發完之後,客戶提出新要求,要求有退款功能,好吧,完整的支付流程也包括退款,幹吧。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. 退款結果通知
這一部分是微信主動發起的異步回調通知,回調地址在前面發起退款申請時可以指定,商戶端需要提供接口接收微信通知,這裏需要註意的是退款結果對重要的數據進行了加密,商戶需要用商戶秘鑰進行解密後才能獲得結果通知的內容。
微信官方給出的解密步驟如下:
- 對加密串A做base64解碼,得到加密串B
- 對商戶key做md5,得到32位小寫key* ( key設置路徑:微信商戶平臺(pay.weixin.qq.com)-->賬戶設置-->API安全-->密鑰設置 )
- 用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 的解決
微信公眾號授權,支付,退款總結
微信支付之退款