1. 程式人生 > >微信支付服務端的一些坑及最終解決

微信支付服務端的一些坑及最終解決

有1年多沒搞微信支付了,最近跳槽,要重做APP,又來接觸微信這個坑比。OK,不多說,上程式碼。以下是我的一個controller類,重點在下面

 // 微信交易型別  
    private static final String TRADETYPE = "APP"; 
    // 微信統一下單介面路徑  
    private static final String UNIFORMORDER = "https://api.mch.weixin.qq.com/pay/unifiedorder"; 
    // 微信回撥地址  
    private static final String NOTIFYURL = "*****";  
    // 微信商戶號:*****  
    private static final String MCHID = "******"; 
    //微信APIKEY  
    private static final String APIKEY ="*******";  
    //微信APPID  
    private static final String APPID ="*******";
    
/**
* @Description: Ajax生成微信預支付ID
* @param entity
* @return JsonResult
* @throws JSONException 
* @throws UnsupportedEncodingException 
*/
@RequestMapping(value = "/getPrepayId",method = RequestMethod.POST)
@ResponseBody
public String getPrepayId(HttpServletRequest request) throws UnsupportedEncodingException {  
        ResultObject result = new ResultObject(false);// 返回資料結果集合  
        request.setCharacterEncoding("UTF-8");    
        try {  
            // 訂單編號  
            String out_trade_no = request.getParameter("out_trade_no") == null ? null : request.getParameter("out_trade_no").trim();// 訂單編號  
            // 消費金額  
            String money = request.getParameter("money") == null ? null : request.getParameter("money").trim();// 消費金額  
            // 消費主題  
            String subject = request.getParameter("subject") == null ? null : request.getParameter("subject").trim();// 消費主體  
            
            if(StringUtils.isEmpty(out_trade_no)){  
                result.setMsg("引數:out_trade_no 為空");  
                result.setResultCode("-1");  
                return JSON.toJSONString(result);  
            }  
            if(StringUtils.isEmpty(money)){  
                result.setMsg("引數:money 為空");  
                result.setResultCode("-1");  
                return JSON.toJSONString(result);  
            }  
            if(StringUtils.isEmpty(subject)){  
                result.setMsg("引數:subject 為空");  
                result.setResultCode("-1");  
                return JSON.toJSONString(result);  
            } 
            String total_fee = "";
            total_fee = String.valueOf((new BigDecimal(money).multiply(new BigDecimal(100))).longValue());
            SortedMap<Object,Object> parame = new TreeMap<Object,Object>();  
            parame.put("appid", APPID);  
            parame.put("mch_id", MCHID);// 商家賬號。  
            String randomStr = getRandomString(18).toUpperCase(); 
            parame.put("nonce_str", randomStr);// 隨機字串       
            parame.put("body", subject);// 商品描述  
            parame.put("out_trade_no", out_trade_no);// 商戶訂單編號  
            parame.put("fee_type", "CNY");//幣種
            parame.put("total_fee", total_fee);// 消費金額  
            String ip = getIpAddr(request);
            if (StringUtils.isEmpty(ip)) {  
                parame.put("spbill_create_ip", "127.0.0.1");// 消費IP地址  
            } else {  
                parame.put("spbill_create_ip", ip);// 消費IP地址  
            }  
            parame.put("notify_url", NOTIFYURL);// 回撥地址  
            parame.put("trade_type", TRADETYPE);// 交易型別APP  
            String sign =createSign(parame);   
            parame.put("sign", sign);// 數字簽證  
            String xml = getRequestXML(parame);   
  
            String content = HttpUtil.sendPost(UNIFORMORDER, xml);    
            System.out.println(content);  
            JSONObject jsonObject = JSONObject.parseObject(XmltoJsonUtil.xml2JSON(content));  
            JSONObject result_xml = jsonObject.getJSONObject("xml");  
            JSONArray result_code =  result_xml.getJSONArray("result_code");              
            String code = (String)result_code.get(0);  
            
            if(code.equalsIgnoreCase("FAIL")){  
                result.setMsg("微信統一訂單下單失敗");  
                result.setResultCode("-1");  
            }else if(code.equalsIgnoreCase("SUCCESS")){  
            result.setFlag(true);
                JSONArray prepay_id = result_xml.getJSONArray("prepay_id");               
                String prepayId = (String)prepay_id.get(0); 
                String timestamp = String.valueOf(System.currentTimeMillis()/1000);
                SortedMap<Object,Object> signParams = new TreeMap<Object,Object>();
        signParams.put("appid", APPID);
        signParams.put("noncestr", randomStr);
        signParams.put("prepayid", prepayId);
        signParams.put("package", "Sign=WXPay");
        signParams.put("partnerid", MCHID);
        signParams.put("timestamp", timestamp);
        String sign2 = createSign(signParams).toUpperCase();
        SortedMap<Object, Object> parameterMap2 = new TreeMap<Object, Object>();  
        parameterMap2.put("appid", APPID);  
                parameterMap2.put("partnerid", MCHID);  
                parameterMap2.put("prepayid", prepayId);  
                parameterMap2.put("package", "Sign=WXPay");  
                parameterMap2.put("noncestr", randomStr);  
                //本來生成的時間戳是13位,但是ios必須是10位,所以截取了一下
               // parameterMap2.put("timestamp", String.valueOf(System.currentTimeMillis()).toString().substring(0,10));  
                parameterMap2.put("timestamp", timestamp);  
                parameterMap2.put("sign", sign2);  
                result.setMsg("微信統一訂單下單成功");  
                result.setResultCode("1");  
                result.setData(parameterMap2);  
            }             
            return JSON.toJSONString(result);  
          
        } catch (Exception e) {  
            result.setMsg(e.getMessage());  
            result.setResultCode("-1");  
            return JSON.toJSONString(result);  
        }  
}


// 返回用IP地址  
    public String getIpAddr(HttpServletRequest request) {  
        String ip = request.getHeader(" x-forwarded-for ");  
        if (ip == null || ip.length() == 0 || " unknown ".equalsIgnoreCase(ip)) {  
            ip = request.getHeader(" Proxy-Client-IP ");  
        }  
        if (ip == null || ip.length() == 0 || " unknown ".equalsIgnoreCase(ip)) {  
            ip = request.getHeader(" WL-Proxy-Client-IP ");  
        }  
        if (ip == null || ip.length() == 0 || " unknown ".equalsIgnoreCase(ip)) {  
            ip = request.getRemoteAddr();  
        }  
        return ip;  
    }  
  
    // 隨機字串生成  
    public static String getRandomString(int length) { // length表示生成字串的長度  
        String base = "abcdefghijklmnopqrstuvwxyz0123456789";  
        Random random = new Random();  
        StringBuffer sb = new StringBuffer();  
        for (int i = 0; i < length; i++) {  
            int number = random.nextInt(base.length());  
            sb.append(base.charAt(number));  
        }  
        return sb.toString();  
    }  
      
      
    //拼接xml 請求路徑  
    public static String getRequestXML(SortedMap<Object, Object> parame){  
        StringBuffer buffer = new StringBuffer();  
        buffer.append("<xml>");  
        Set set = parame.entrySet();  
        Iterator iterator = set.iterator();  
        while(iterator.hasNext()){  
            Map.Entry entry = (Map.Entry) iterator.next();  
            String key = (String)entry.getKey();  
            String value = (String)entry.getValue();  
            //過濾相關欄位sign  
            if("sign".equalsIgnoreCase(key)){  
                buffer.append("<"+key+">"+"<![CDATA["+value+"]]>"+"</"+key+">");  
            }else{  
                buffer.append("<"+key+">"+value+"</"+key+">");  
            }             
        }  
        buffer.append("</xml>");  
        return buffer.toString();  
    }  
      
    //建立md5 數字簽證  
    public static String createSign(SortedMap<Object, Object> parame){  
        StringBuffer buffer = new StringBuffer();  
        Set set = parame.entrySet();  
        Iterator iterator = set.iterator();  
        while(iterator.hasNext()){  
            Map.Entry entry = (Map.Entry) iterator.next();  
            String key = (String)entry.getKey();  
            Object value = (String)entry.getValue();  
            if(null != value && !"".equals(value) && !"sign".equals(key) && !"key".equals(key)){  
                buffer.append(key+"="+value+"&");  
            }             
        }  
        buffer.append("key="+APIKEY);  
        System.out.println(buffer.toString());
        String sign = MD5.getMessageDigest(buffer.toString().getBytes()).toUpperCase();
        System.out.println("簽名引數:"+sign);  
        return sign;  
    }

第一步,生成prepayid,這一步,只要你的appid,mch_id,key沒寫錯,那麼99%以上都能獲取到prepayid,如果失敗,那肯定是幾個ID和key有問題,仔細檢查,包括編碼等,仔細仔細仔細檢查。

問題來了,第二步,對獲取到的prepayid進行二次簽名,官方文件的坑來了,官方並沒有詳細說明這一步驟,一切的一切只能靠自己摸索,爬坑。

首先第一坑:引數順序,我這裡用了SortedMap,自動對引數進行asc編碼順序,一勞永逸,當然,也可以用其他map,但一定要注意引數順序,必須是asc編碼順序。

第二坑:引數package的Sign=WXPay中=的編碼問題,轉碼即可,小坑。

第三坑:

蘋果系統的timestamp位數,統一成10位即可,小坑。

第四坑:次級大坑,注意,官方文件說到的參與二次簽名的引數,prepayId,appId,timeStamp等,如果你用他們的駝峰進行大寫,那麼你就完了。一定要小寫,小寫,小寫。

第五坑:最大坑,一樣,官方文件並沒有對於二次簽名有過多贅述,如果你上面幾個坑完美出坑,那麼,你獲取到的簽名sign跟官方驗證的sign絕對是一樣的,然而,將這些玩意丟回給APP,APP調起支付,大大的幾個字出現了,驗證簽名失敗!WTF!不要急,我已折騰了好幾天,終於發現坑在哪裡,那就是noncestr隨機字串,參與二次簽名的隨機字串不能再次生成,注意,不能再次生成,一定要用第一步中獲取prepayid時的那串字串,一定要用第一步中獲取prepayid時的那串字串,一定要用第一步中獲取prepayid時的那串字串。

第六坑:經歷了上述5坑,相信你已經有想幹死人的衝動,那麼你以為這就結束了嗎,還有最後一坑,那就是APP簽名已經包名,一定要與開放平臺中的一致,然而,即使一致了你以為又結束了嗎,NO,如果你更改過開放平臺中的簽名,並且,在更改前呼叫過APP微信支付,那麼一定一定一定一定記得清除微信快取。

至此,所有坑都成功出坑,終於出現了支付頁面,舉國歡騰,微信去年買了個表。最後附上MD5簽名類

import java.security.MessageDigest;


public class MD5 {


private MD5() {}

public final static String getMessageDigest(byte[] buffer) {
char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
try {
MessageDigest mdTemp = MessageDigest.getInstance("MD5");
mdTemp.update(buffer);
byte[] md = mdTemp.digest();
int j = md.length;
char str[] = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
return new String(str);
} catch (Exception e) {
return null;
}
}
}