写在最前:关于文中涉及到隐私的部分已经隐去,只做技术分享。
先简单介绍一下背景,公司app要改成H5混合开发需要对原有的app后台进行移植,增加新的接
口功能满足新需求。在移植的时候我直接把原来的微信支付模块全部搬过来,前端那边微信支付一直没做
好,后台这边我也没法测试移植的模块能否正常支付。等前端把微信支付部分做好以后,我才开始测试这
部分代码,本想着之前app中都用得好好的,现在应该也没什么问题,实际上这时我已经掉进了微信支付
的坑里,给大家分享一下我爬过的坑,免得后来的人重蹈覆辙。
照例还是先简单介绍一下微信APP支付的思路:
1、从前台获取到用户提交的订单参数,生成第一次签名。
2、调用微信统一下单接口URL https://api.mch.weixin.qq.com/pay/unifiedorder 生成预支付id:
prepayId。
3、对参数进行二次签名。
4、将所需的数据传给前台,调起微信支付。
5、微信将将支付通知给后台。
6、后台执行回调操作,完成整个支付流程。
这是官方给出的流程:
https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=8_3
本地测试,预支付id已经成功获取到了,说明1、2步都没有问题。
这里二次签名的参数也已经拿到,但是前台调起微信之后提示支付验证签名失败。
至此,我遇到的问题就出现在第三步,对参数进行二次签名,所有的参数都获取到了,传给前台调起微信一直提示签名无效。
然后我就围绕二次签名这里去找问题出现的原因。因为是二次移植,刚开始怀疑是微信开发者后台参数秘
钥这些的有误,登上后台看参数都没问题。然后想可能是微信的工具类中生成二次签名传的参数不全,把
该传的参数都传入以后还是同一个错误。最后我把目光转向二次签名的参数名称上面,想着这么重要的东
西应该不会出错吧,查阅官方文档在APP支付里的业务流程中发现了猫腻。
下面这图是该文档16年和17年不同时间的对比图,看完以后终于发现问题所在。之前项目做的时候,字段名是有大写的,现在再看已经全都改成小写了。而且发放在这么不起眼的地方,也没个提示啥的。真是给微信跪了,不带这么坑人的。
我把参数名称全改成小写以后,终于能够顺利完成支付了,说起来真是一把辛酸泪!
说完整个流程把部分支付代码贴出来,供大家参考。
商户号、商户秘钥、APP秘钥、回调地址等配置参数这些的就略过了,用的时候提前配置好就行。
这个代码段里构造请求参数。先构造第一次签名的参数,获得requestSign。然后构造xml参数调用微信统一下单接口URL获得预支付id。接着构造第二次签名的参数,调用同第一次的签名方法生成最终传给前台的sign签名参数。最后,将所有参数一并传给前台。
String currTime = TenpayUtil.getCurrTime();
//八位日期相关的字符串
String strTime = currTime.substring(8, currTime.length());
//四位随机数的字符串
String strRandom = TenpayUtil.buildRandom(4) + "";
//十位随机字符串
String nonceStr = strTime + strRandom;
/**
* 商品描述,订单号,订单总金额从手机端获取
*/
String spbillCreateIp = request.getRemoteAddr();
SortedMap<String, String> requestParams = new TreeMap<String, String>();
requestParams.put("appid", Constant.APP_ID);
requestParams.put("mch_id", Constant.PARTNER);
requestParams.put("nonce_str", nonceStr);
requestParams.put("body", body);//商品描述
requestParams.put("out_trade_no", outTradeNo);//订单号
requestParams.put("total_fee", String.valueOf(prices));//订单总金额
requestParams.put("spbill_create_ip", spbillCreateIp);//APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器ip
requestParams.put("notify_url", Constant.WALLET_NOTIFIY_URL);//通知地址
requestParams.put("trade_type", "APP");//交易类型
RequestHandler reqHandler = new RequestHandler(request, response);
reqHandler.init(Constant.APP_ID, Constant.APP_SECRET, Constant.PARTNER_KEY);
String requestSign = reqHandler.createSign(requestParams);
//构造请求参数
String xml = "<xml>"
+ "<appid>" + Constant.APP_ID + "</appid>"
+ "<mch_id>" + Constant.PARTNER + "</mch_id>"
+ "<nonce_str>" + nonceStr + "</nonce_str>"
+ "<sign>" + requestSign + "</sign>"
+ "<body><![CDATA[" + body + "]]></body>"
+ "<out_trade_no>" + outTradeNo + "</out_trade_no>"
+ "<total_fee>" + prices + "</total_fee>"
+ "<spbill_create_ip>" + spbillCreateIp + "</spbill_create_ip>"
+ "<notify_url>" + Constant.WALLET_NOTIFIY_URL + "</notify_url>"
+ "<trade_type>APP</trade_type>"
+ "</xml>";
new GetWxOrderNo();
String prepayId = GetWxOrderNo.getPayNo(Constant.PRE_URL, xml);//获得预支付单信息
System.out.println("获取到的预支付ID:" + prepayId);
/**
* 统一下单接口返回正常的prepay_id,再按签名规范重新生成签名后,将数据传输给APP。
* 参与签名的字段名为appId,partnerId,prepayId,nonceStr,timeStamp,package。
* 注意:package的值格式为Sign=WXPay
* ~~~~~~~~2017年微信文档上字段名都要小写!!!!!
*/
SortedMap<String, String> getParams = new TreeMap<String, String>();
String timeStamp = Sha1Util.getTimeStamp();
getParams.put("appid", Constant.APP_ID);
getParams.put("partnerid", Constant.PARTNER);
getParams.put("prepayid", prepayId);
getParams.put("noncestr", nonceStr);
getParams.put("timestamp", timeStamp);
getParams.put("package", "Sign=WXPay");
String getSign = reqHandler.createSign(getParams);
System.out.println("--微信充值 ---"+getSign);
map1.put("msg", "成功");//当统一下订单接口执行完成之后,返回调起支付接口所需要的所有参数给APP
map2.put("appId", Constant.APP_ID);
map2.put("partnerId", Constant.PARTNER);
map2.put("prepayId", prepayId);
map2.put("nonceStr", nonceStr);
map2.put("timeStamp", timeStamp);
map2.put("package", "Sign=WXPay");
map2.put("sign", getSign);
下图红框中的参数就是二次签名所需要的参数,一定要注意参数名称的大小写,目前官网参考文档都是小写,以后会不会再变还是未知数。还有需要注意的是timestamp时间戳一定要是10位,别问我为什么,因为官方就是这样规定的。
最后贴出工具类中比较重要的一个签名方法。
首先要对参数进行排序,在最末尾拼接上商户私钥,接着进行MD5加密编码格式为UTF-8,最后将加密后的参数转成大写,完成签名。
/**
* 创建md5摘要,规则是:按参数名称a-z排序,遇到空值的参数不参加签名。
*/
public String createSign(SortedMap<String, String> packageParams) {
StringBuffer sb = new StringBuffer();
Set es = packageParams.entrySet();
Iterator it = es.iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String k = (String) entry.getKey();
String v = (String) entry.getValue();
if (null != v && !"".equals(v) && !"sign".equals(k)
&& !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + this.getKey());
System.out.println("md5 sb:" + sb);
String sign = MD5Util.MD5Encode(sb.toString(), this.charset)
.toUpperCase();
System.out.println("packge签名:" + sign);
return sign;
}
将所有参数传给前台,完成微信支付,回调中验证支付完成,剩下的就写自己的业务逻辑,先略过了。至此,整个微信APP支付流程就走完了。
在实际开发中,可能每个人遇到的情况和我并不一定相同,仅给大家一个参考借鉴。希望能给困惑中你带来一些启发和帮助爬出微信支付的坑。
附上用到的工具类下载链接 点我下载
好了,有时间再把支付宝APP支付的坑也一填,整理出来分享给大家。