記得上次接觸微信支付是2016年末,那次也是我程序生涯中首次碰及支付業務,慌張談不上可是懵逼懷疑時時都有。提及第三方登陸或者支付,想必都清楚是直接調用人家現成的API,徹底沒有開發成本和技術含量。可是我想說:若是你沒有過一次從零開發並維護過一個完整支付模塊的話,我相信有不少坑對你來講都是黑盒的,也許如今的公司或多或少都涵蓋了支付業務,可能被早先的老程序員們已經維護的差很少完美了,徹底是能夠當成模板功能來使用,新手只需複製大致功能架子填充支付後的業務邏輯便可。html
好了,切入正題吧。爲何忽然在各位前輩面前擺上這麼一篇沒有任何技術含量的文章呢?由於很久沒有在博客園發佈過文章了,剛剛瞻仰完各位大佬分享的東西后內心很空虛。其次是主要緣由,因爲前幾天休假剛回來就被剛剛入職的哥們拉住繞了一下子,說這倆天測試人員反應充一次錢以後會看到好幾條充值記錄而且是偶現的,並且相應的遊戲幣也累加了不少次等等,而後還有一個更坑的現象就是照着微信開發文檔調用接口,喚醒微信支付的組件開始調用預支付(統一下單)接口時拋簽名錯誤的異常,居然調不通(聲明:簽名方式已是確保無誤的)。。。真的,聽他一說還真回憶起當年那個手忙腳亂的本身,在開發過程當中各類懵逼、各類懷疑人生。因此今天就把這些踩坑經歷分享一下,僅獻給圈裏剛剛接觸或要開始接手支付業務的朋友,但願各位在鑄造支付模塊時可以一馬平川。前端
一、登陸微信商戶平臺,注意是商戶平臺不是開放平臺,根據業務場景選擇適合本身的支付類型,進入以後就能夠看看具體的API列表,除此以外還提供了業務場景舉例、業務流程時序圖等很是清晰。提醒一點微信還提供了專門的demo壓縮包,裏面包含了工具類WXPayUtil建議下載,由於相信你能用到,是百分百能用到。java
1 package com.qy.utils; 2 3 import java.io.ByteArrayInputStream; 4 import java.io.InputStream; 5 import java.io.StringWriter; 6 import java.util.*; 7 import java.security.MessageDigest; 8 import org.w3c.dom.Node; 9 import org.w3c.dom.NodeList; 10 11 import com.qy.utils.WXPayConstants.SignType; 12 13 import javax.crypto.Mac; 14 import javax.crypto.spec.SecretKeySpec; 15 import javax.xml.parsers.DocumentBuilder; 16 import javax.xml.parsers.DocumentBuilderFactory; 17 import javax.xml.transform.OutputKeys; 18 import javax.xml.transform.Transformer; 19 import javax.xml.transform.TransformerFactory; 20 import javax.xml.transform.dom.DOMSource; 21 import javax.xml.transform.stream.StreamResult; 22 import org.slf4j.Logger; 23 import org.slf4j.LoggerFactory; 24 25 26 public class WXPayUtil { 27 28 /** 29 * XML格式字符串轉換爲Map 30 * 31 * @param strXML XML字符串 32 * @return XML數據轉換後的Map 33 * @throws Exception 34 */ 35 public static Map<String, String> xmlToMap(String strXML) throws Exception { 36 try { 37 Map<String, String> data = new HashMap<String, String>(); 38 DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); 39 DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); 40 InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8")); 41 org.w3c.dom.Document doc = documentBuilder.parse(stream); 42 doc.getDocumentElement().normalize(); 43 NodeList nodeList = doc.getDocumentElement().getChildNodes(); 44 for (int idx = 0; idx < nodeList.getLength(); ++idx) { 45 Node node = nodeList.item(idx); 46 if (node.getNodeType() == Node.ELEMENT_NODE) { 47 org.w3c.dom.Element element = (org.w3c.dom.Element) node; 48 data.put(element.getNodeName(), element.getTextContent()); 49 } 50 } 51 try { 52 stream.close(); 53 } catch (Exception ex) { 54 // do nothing 55 } 56 return data; 57 } catch (Exception ex) { 58 WXPayUtil.getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML); 59 throw ex; 60 } 61 62 } 63 64 /** 65 * 將Map轉換爲XML格式的字符串 66 * 67 * @param data Map類型數據 68 * @return XML格式的字符串 69 * @throws Exception 70 */ 71 public static String mapToXml(Map<String, String> data) throws Exception { 72 DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); 73 DocumentBuilder documentBuilder= documentBuilderFactory.newDocumentBuilder(); 74 org.w3c.dom.Document document = documentBuilder.newDocument(); 75 org.w3c.dom.Element root = document.createElement("xml"); 76 document.appendChild(root); 77 for (String key: data.keySet()) { 78 String value = data.get(key); 79 if (value == null) { 80 value = ""; 81 } 82 value = value.trim(); 83 org.w3c.dom.Element filed = document.createElement(key); 84 filed.appendChild(document.createTextNode(value)); 85 root.appendChild(filed); 86 } 87 TransformerFactory tf = TransformerFactory.newInstance(); 88 Transformer transformer = tf.newTransformer(); 89 DOMSource source = new DOMSource(document); 90 transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); 91 transformer.setOutputProperty(OutputKeys.INDENT, "yes"); 92 StringWriter writer = new StringWriter(); 93 StreamResult result = new StreamResult(writer); 94 transformer.transform(source, result); 95 String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", ""); 96 try { 97 writer.close(); 98 } 99 catch (Exception ex) { 100 } 101 return output; 102 } 103 104 105 /** 106 * 生成帶有 sign 的 XML 格式字符串 107 * 108 * @param data Map類型數據 109 * @param key API密鑰 110 * @return 含有sign字段的XML 111 */ 112 public static String generateSignedXml(final Map<String, String> data, String key) throws Exception { 113 return generateSignedXml(data, key, SignType.MD5); 114 } 115 116 /** 117 * 生成帶有 sign 的 XML 格式字符串 118 * 119 * @param data Map類型數據 120 * @param key API密鑰 121 * @param signType 簽名類型 122 * @return 含有sign字段的XML 123 */ 124 public static String generateSignedXml(final Map<String, String> data, String key, SignType signType) throws Exception { 125 String sign = generateSignature(data, key, signType); 126 data.put(WXPayConstants.FIELD_SIGN, sign); 127 return mapToXml(data); 128 } 129 130 131 /** 132 * 判斷簽名是否正確 133 * 134 * @param xmlStr XML格式數據 135 * @param key API密鑰 136 * @return 簽名是否正確 137 * @throws Exception 138 */ 139 public static boolean isSignatureValid(String xmlStr, String key) throws Exception { 140 Map<String, String> data = xmlToMap(xmlStr); 141 if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) { 142 return false; 143 } 144 String sign = data.get(WXPayConstants.FIELD_SIGN); 145 return generateSignature(data, key).equals(sign); 146 } 147 148 /** 149 * 判斷簽名是否正確,必須包含sign字段,不然返回false。使用MD5簽名。 150 * 151 * @param data Map類型數據 152 * @param key API密鑰 153 * @return 簽名是否正確 154 * @throws Exception 155 */ 156 public static boolean isSignatureValid(Map<String, String> data, String key) throws Exception { 157 return isSignatureValid(data, key, SignType.MD5); 158 } 159 160 /** 161 * 判斷簽名是否正確,必須包含sign字段,不然返回false。 162 * 163 * @param data Map類型數據 164 * @param key API密鑰 165 * @param signType 簽名方式 166 * @return 簽名是否正確 167 * @throws Exception 168 */ 169 public static boolean isSignatureValid(Map<String, String> data, String key, SignType signType) throws Exception { 170 if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) { 171 return false; 172 } 173 String sign = data.get(WXPayConstants.FIELD_SIGN); 174 return generateSignature(data, key, signType).equals(sign); 175 } 176 177 /** 178 * 生成簽名 179 * 180 * @param data 待簽名數據 181 * @param key API密鑰 182 * @return 簽名 183 */ 184 public static String generateSignature(final Map<String, String> data, String key) throws Exception { 185 return generateSignature(data, key, SignType.MD5); 186 } 187 188 /** 189 * 生成簽名. 注意,若含有sign_type字段,必須和signType參數保持一致。 190 * 191 * @param data 待簽名數據 192 * @param key API密鑰 193 * @param signType 簽名方式 194 * @return 簽名 195 */ 196 public static String generateSignature(final Map<String, String> data, String key, SignType signType) throws Exception { 197 Set<String> keySet = data.keySet(); 198 String[] keyArray = keySet.toArray(new String[keySet.size()]); 199 Arrays.sort(keyArray); 200 StringBuilder sb = new StringBuilder(); 201 for (String k : keyArray) { 202 if (k.equals(WXPayConstants.FIELD_SIGN)) { 203 continue; 204 } 205 if (data.get(k).trim().length() > 0) // 參數值爲空,則不參與簽名 206 sb.append(k).append("=").append(data.get(k).trim()).append("&"); 207 } 208 sb.append("key=").append(key); 209 if (SignType.MD5.equals(signType)) { 210 return MD5(sb.toString()).toUpperCase(); 211 } 212 else if (SignType.HMACSHA256.equals(signType)) { 213 return HMACSHA256(sb.toString(), key); 214 } 215 else { 216 throw new Exception(String.format("Invalid sign_type: %s", signType)); 217 } 218 } 219 220 221 /** 222 * 獲取隨機字符串 Nonce Str 223 * 224 * @return String 隨機字符串 225 */ 226 public static String generateNonceStr() { 227 return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32); 228 } 229 230 231 /** 232 * 生成 MD5 233 * 234 * @param data 待處理數據 235 * @return MD5結果 236 */ 237 public static String MD5(String data) throws Exception { 238 java.security.MessageDigest md = MessageDigest.getInstance("MD5"); 239 byte[] array = md.digest(data.getBytes("UTF-8")); 240 StringBuilder sb = new StringBuilder(); 241 for (byte item : array) { 242 sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3)); 243 } 244 return sb.toString().toUpperCase(); 245 } 246 247 /** 248 * 生成 HMACSHA256 249 * @param data 待處理數據 250 * @param key 密鑰 251 * @return 加密結果 252 * @throws Exception 253 */ 254 public static String HMACSHA256(String data, String key) throws Exception { 255 Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); 256 SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256"); 257 sha256_HMAC.init(secret_key); 258 byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8")); 259 StringBuilder sb = new StringBuilder(); 260 for (byte item : array) { 261 sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3)); 262 } 263 return sb.toString().toUpperCase(); 264 } 265 266 /** 267 * 日誌 268 * @return 269 */ 270 public static Logger getLogger() { 271 Logger logger = LoggerFactory.getLogger("wxpay java sdk"); 272 return logger; 273 } 274 275 /** 276 * 獲取當前時間戳,單位秒 277 * @return 278 */ 279 public static long getCurrentTimestamp() { 280 return System.currentTimeMillis()/1000; 281 } 282 283 /** 284 * 獲取當前時間戳,單位毫秒 285 * @return 286 */ 287 public static long getCurrentTimestampMs() { 288 return System.currentTimeMillis(); 289 } 290 291 /** 292 * 生成 uuid, 即用來標識一筆單,也用作 nonce_str 293 * @return 294 */ 295 public static String generateUUID() { 296 return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32); 297 } 298 299 }
二、移動端喚起微信支付的組件,首先發起開始支付的動做,服務端就會相應的調用預支付(統一下單)接口https://api.mch.weixin.qq.com/pay/unifiedorder,注意該過程須要對參數進行簽名,而後將帶有簽名字段的參數做爲接口形參,過程比較繁瑣建議封裝一下,後續的操做還有會用到的,最後拿到微信服務器返回的結果後解析出移動端須要的參數,記得簽名後返給前端。好了部分源碼能夠參考一下:node
1 String body = "抓樂GO"; 2 String nonceStr = WXPayUtil.generateNonceStr(); //獲取隨機字符串 3 String outTradeNo = WXPayUtil.generateUUID(); //商戶訂單號(本次交易訂單號) 4 String tradeType = "APP"; //支付類型:APP 5 Double money = tokenInfo.getMoney() * 100.0; //金額(單位/分) 6 //int totalFee = (new Double(money)).intValue(); 7 int totalFee = (int) Math.ceil(money); 8 logger.info("------------------支付金額爲:{}",totalFee); 9 String parames = "<xml><appid>"+appID+"</appid><body>"+body+"</body><mch_id>" 10 +mchID+"</mch_id><nonce_str>"+nonceStr+"</nonce_str><notify_url>"+notifyUrl 11 +"</notify_url><out_trade_no>"+outTradeNo+"</out_trade_no><spbill_create_ip>"+spbillCreateIp 12 +"</spbill_create_ip><total_fee>"+totalFee+"</total_fee><trade_type>"+tradeType 13 +"</trade_type></xml>"; 14 Map<String, String> mapXML = WXPayUtil.xmlToMap(parames); //將不包含sign字段的xml字符串轉成map 15 String sign = WXPayUtil.generateSignature(mapXML, appKey); //生成簽名 16 17 String signParames = "<xml><appid>"+appID+"</appid><body>"+body+"</body><mch_id>" 18 +mchID+"</mch_id><nonce_str>"+nonceStr+"</nonce_str><notify_url>"+notifyUrl 19 +"</notify_url><out_trade_no>"+outTradeNo+"</out_trade_no><spbill_create_ip>"+spbillCreateIp 20 +"</spbill_create_ip><total_fee>"+totalFee+"</total_fee><trade_type>"+tradeType 21 +"</trade_type><sign>"+sign+"</sign></xml>"; //預支付接口xml參數(包含sign) 22 Map<String, String> mapXML1 = WXPayUtil.xmlToMap(signParames); 23 boolean boo = WXPayUtil.isSignatureValid(mapXML1, appKey); //校驗簽名是否正確
1 if("SUCCESS".equals(dataMap.get("result_code"))){ 2 logger.info("預支付接口調用成功:"); 3 //預支付調用成功 4 //二次簽名 5 Map<String,String> signMap = new LinkedHashMap<String,String>(); 6 signMap.put("appid", dataMap.get("appid")); 7 signMap.put("partnerid", dataMap.get("mch_id")); 8 signMap.put("prepayid", dataMap.get("prepay_id")); 9 signMap.put("package", "Sign=WXPay"); 10 signMap.put("noncestr", WXPayUtil.generateNonceStr()); 11 signMap.put("timestamp", String.valueOf(System.currentTimeMillis()/1000)); 12 String appSign = WXPayUtil.generateSignature(signMap, appKey); 13 signMap.put("sign", appSign); 14 signMap.put("outTradeNo", outTradeNo); //支付訂單號 15 //String signXml = WXPayUtil.mapToXml(signMap); 16 //System.out.println(signXml); 17 dataJson.put("code", Constants.HTTP_RESPONSE_SUCCESS); 18 dataJson.put("msg", Constants.HTTP_RESPONSE_SUCCESS_MSG); 19 dataJson.put("data", signMap); 20 logger.info("返給APP的二次簽名數據:{}",signMap);
三、移動端拿到支付參數後就會真正調起支付操做,這時後端只需作一個供微信回調的接口,該接口的做用主要是接受每次支付的支付結果。注意:該回調地址須要在發起預支付接口時務必告訴微信服務器,並且還要保證可以暢通無阻。當完成一筆支付操做後,微信服務器就馬上會調用你提供的自定義回調接口告訴你支付結果,你只需完成支付成功後的業務邏輯,即視爲本次支付過程結束。程序員
1 public String payCallback(HttpServletRequest request, HttpServletResponse response){ 2 BufferedReader reader = null; 3 try { 4 reader = request.getReader(); 5 String line = ""; 6 String xmlString = null; 7 StringBuffer inputString = new StringBuffer(); 8 while ((line = reader.readLine()) != null) { 9 inputString.append(line); 10 } 11 xmlString = inputString.toString(); 12 if(WXPayUtil.isSignatureValid(xmlString,appKey)){ 13 logger.info("微信支付結果{}",xmlString); 14 request.getReader().close(); 15 Map<String, String> resultMap = WXPayUtil.xmlToMap(xmlString); //支付結果 16 if("SUCCESS".equals(resultMap.get("return_code"))){
支付成功後的業務邏輯....
四、固然,微信也專門提供了查詢某筆支付訂單的支付結果的接口https://api.mch.weixin.qq.com/pay/orderquery,詳情自行查詢。數據庫
1 public String getOrderResult(String outTradeNo){ 2 logger.info("查詢支付訂單號{}支付結果================",outTradeNo); 3 Map<String,Object> dataJson = new LinkedHashMap<String,Object>(); 4 try { 5 String nonceStr = WXPayUtil.generateNonceStr(); //獲取隨機字符串 6 //請求參數 7 String parames = "<xml><appid>"+appID+"</appid><mch_id>"+mchID+"</mch_id>" 8 +"<nonce_str>"+nonceStr+"</nonce_str>" 9 + "<out_trade_no>"+outTradeNo+"</out_trade_no></xml>"; 10 String signXMLData = WXPayUtil.generateSignedXml(WXPayUtil.xmlToMap(parames), appKey); //生成帶有簽名的xml 11 String queryResult = HttpClient.doPostXML(orderQueryURL, signXMLData); //查詢結果xml 12 if(WXPayUtil.isSignatureValid(queryResult,appKey)){ 13 Map<String, String> wxPayResult = WXPayUtil.xmlToMap(queryResult); //微信支付的結果通知
OK,到這兒整個支付業務算是真正跑通了,勉強畫條暫時的分割線吧。後端
這個問題確實對於不少新手來講是狠TM扯淡的,調不通還老提示簽名錯誤多是由於:http請求的參數列表中body那個字段你傳的是中文,而且微信開發文檔中的案例模板也是中文。api
解決方案:只需將最終發送的參數列表進行編碼處理便可,可是你也能夠所有傳入英文。安全
1 //若是校驗經過,則調用預支付 2 logger.info("開始調用微信預支付接口:https://api.mch.weixin.qq.com/pay/unifiedorder"); 3 signParames = new String(signParames.getBytes("UTF-8"), "ISO-8859-1"); 4 String result = HttpClient.doPostXML(payURL, signParames); 5 Map<String, String> dataMap = WXPayUtil.xmlToMap(result); 6 logger.info("預支付結果:{}",result);
這個問題就有點考驗你寫接口的質量了,出現僅支付一次產生多條支付明細記錄的狀況,首先是由於你沒有作好在成功拿到微信回調結果後及時對當前支付記錄作好重複處理的邏輯,由於那哥們兒發起一筆支付請求後在成功拿到支付結果沒有告訴微信支付成功,因此微信認爲通知失敗,微信會經過必定的策略按期從新發起通知,儘量提升通知的成功率,但微信不保證通知最終能成功,通知頻率爲15/15/30/180/1800/1800/1800/1800/3600,單位:秒。因此不斷處理同一筆支付訂單。其次就是沒有考慮併發的狀況,須要對拿到的回調結果作線程安全的處理,能夠有倆種方案:第一種就是在數據庫層面上作限制,設置聯合主鍵將重複操做的支付記錄數據擋在外面不容許插入數據庫;第二種是在業務層加鎖,在處理每筆支付結果時判斷是否已經處理過了,若是處理過就忽略當前回調結果不然正常處理。提醒一點:不要直接在方法上直接添加synchronized,還有在加鎖的時候儘可能將鎖的粒度控制到最小,不然會影響接口的性能。(參考:http://www.cnblogs.com/1315925303zxz/p/7561236.html)服務器