微信支付服務端的一些坑及最終解決
有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中=的編碼問題,轉碼即可,小坑。
第三坑:
第四坑:次級大坑,注意,官方文件說到的參與二次簽名的引數,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;
}
}
}