1、微信支付的流程

如下三张手机截图,我们在微信网页端看到的支付,表面上看到的是 “点击支付按钮 - 弹出支付框 - 支付成功后出现提示页面”,实际上的核心处理过程是:
  • 点击支付按钮时,执行一个Ajax到后台
  • 后台通过前台的部分信息(如商品名额,金额等),将其组装成符合微信要求格式的xml,然后调用微信的“统一下单接口”
  • 调用成功后微信会返回一个组装好的xml,我们提取之中的消息(预支付id也在其中)以JSON形式返回给前台
  • 前台将该JSON传参给微信内置JS的方法中,调其微信支付
  • 支付成功后,微信会将本次支付相关信息返回给我们的服务器

这些在《微信支付官方文档 - 场景介绍》和《微信支付官方文档 - 业务流程》都进行了更详细的说明。




2、微信支付功能开发详解

2.1 设置支付目录和授权域名

登陆公众号,进行支付相关的目录和域名设置,详情参考《微信支付官方文档 - 开发步骤》,我这里简单贴几张官方的图就行了,这步比较简单,就不过多说明了,只提其中一点:对于微信支付授权的目录,发起微信支付的页面必须精确地位于授权目录下,假如支付页面为 http://www.a.com/wx/pay/a.html,那么授权目录必须为 http://www.a.com/wx/pay/,其他的如 http://www.a.com/wx/ , https://www.a.com/wx/pay/(http和https是不一样的),http://a.com/wx/pay/(千万别忘了www)都是不行的。填写了这些非法目录无法调起支付。






2.2 组装xml,调用统一下单接口,获取prepay_id

2.2.1 组装xml

点击支付按钮后,写一个Ajax将前台部分信息发送给后台,然后组装xml,调用统一下单接口。该接口在《微信支付官方文档 - 统一下单》进行了很详细的解释,我在这里进行部分说明:
参数 说明     备注
appId     开发者应用ID,在 “开发 - 基本配置” 查看
mch_id 微信支付的商户号,在 “微信支付 - 商户信息” 查看
device_info     终端设备号(门店号或收银设备ID) PC网页或公众号内支付,则传 “WEB”
body 商品或支付的简单描述
trade_type 可取值JSAPI,NATIVE,APP等,我们这里使用的是JSAPI JSAPI 公众号支付;NATIVE 原生扫码支付;APP app支付
nonce_str 随机字符串 参考算法:《微信支付官方文档 - 安全规范
notify_url 通知地址,微信支付成功后,微信服务器会发送信息到该url
out_trade_no 商户系统内部订单号,由商户自定义,订单号要保持唯一性
total_fee 订单总金额,单位:分
openid 用户标识,用户在该公众号下的唯一身份标识
sign 签名 参考算法:《微信支付官方文档 - 安全规范
key API密钥,在 “微信商户平台 - 账户中心 - API安全 - API密钥”

其他的都比较简单,重要的在于这两个涉及算法的参数,nonce_str 和 sign,这里说明一下:

  • nonce_str 随机字符串,用于保证签名不可预测
    • 算法:
    • 官方建议调用随机数函数生成,然后转为字符串

  • sign 签名
    • 算法:
    • 所有发送或接收的数据按参数名ASCII码从小到大排序,使用键值对形式拼接为字符串(如 key1=value1&key2=value2…)
    • ASCII码的字典排序,可以利用TreeMap帮我们自动实现
    • 将拼接好的字符串最后,再拼接上API密钥,即key,得到新的字符串
    • 将新的字符串进行MD5加密,并将加密后字符串全部转换为大写

按照以上的这些说明,进行xml的拼装,贴上我自己的测试代码(注:为方便测试,部分数据我直接写入方法了,如body、openId等):
String appId = WeChatAPI.getAppID();
String body = "JSAPI支付测试";
String merchantId = WeChatAPI.getMerchantID();
String tradeNo = String.valueOf(new Date().getTime());
String nonceStr1 = SignUtil.createNonceStr();
String notifyUrl = "http://k169710n05.51mypc.cn/pay/do/afterPaySuccess.q";
String openId = "okAkc0muYuSJUtvMf25UQHnqYvM4";
String totalFee = "1"; TreeMap<String, String> map = new TreeMap<String, String>();
map.put("appid", appId);
map.put("mch_id", merchantId);
map.put("device_info", "WEB");
map.put("body", body);
map.put("trade_type", "JSAPI");
map.put("nonce_str", nonceStr1);
map.put("notify_url", notifyUrl);
map.put("out_trade_no", tradeNo);
map.put("total_fee", totalFee);
map.put("openid", openId);
String sign = SignUtil.createSign(map); String xml = "<xml>" +
"<appid>" + appId + "</appid>" +
"<body>" + body +"</body>" +
"<device_info>WEB</device_info>" +
"<mch_id>" + merchantId + "</mch_id>" +
"<nonce_str>" + nonceStr1 + "</nonce_str>" +
"<notify_url>" + notifyUrl +"</notify_url>" +
"<openid>" + openId + "</openid>" +
"<out_trade_no>" + tradeNo + "</out_trade_no>" +
"<total_fee>" + totalFee + "</total_fee>" +
"<trade_type>JSAPI</trade_type>" +
"<sign>" + sign + "</sign>" +
"</xml>";
35
 
1
String appId = WeChatAPI.getAppID(); 
2
String body = "JSAPI支付测试";
3
String merchantId = WeChatAPI.getMerchantID();
4
String tradeNo = String.valueOf(new Date().getTime());
5
String nonceStr1 = SignUtil.createNonceStr();
6
String notifyUrl = "http://k169710n05.51mypc.cn/pay/do/afterPaySuccess.q";
7
String openId = "okAkc0muYuSJUtvMf25UQHnqYvM4";
8
String totalFee = "1";
9

10
TreeMap<String, String> map = new TreeMap<String, String>();
11
map.put("appid", appId);
12
map.put("mch_id", merchantId);
13
map.put("device_info", "WEB");
14
map.put("body", body);
15
map.put("trade_type", "JSAPI");
16
map.put("nonce_str", nonceStr1);
17
map.put("notify_url", notifyUrl);
18
map.put("out_trade_no", tradeNo);
19
map.put("total_fee", totalFee);
20
map.put("openid", openId);
21
String sign = SignUtil.createSign(map);
22

23
String xml = "<xml>" +
24
                "<appid>" + appId + "</appid>" +
25
                "<body>" + body +"</body>" +
26
                "<device_info>WEB</device_info>" +
27
                "<mch_id>" + merchantId + "</mch_id>" +
28
                "<nonce_str>" + nonceStr1 + "</nonce_str>" +
29
                "<notify_url>" + notifyUrl +"</notify_url>" +
30
                "<openid>" + openId + "</openid>" +
31
                "<out_trade_no>" + tradeNo + "</out_trade_no>" +
32
                "<total_fee>" + totalFee + "</total_fee>" +
33
                "<trade_type>JSAPI</trade_type>" +
34
                "<sign>" + sign + "</sign>" +
35
             "</xml>";

注意:
  • body参数如果直接填写中文,在调用接口时会出现“签名错误”,要以ISO8859-1编码
  • 所以 String body = new String("body内容字符串".getBytes("ISO8859-1"));
  • 但即便如此,在支付完成后微信推送的“微信支付凭证”中,商品详情中的中文也依然显示的乱码

  • body参数内容如果包含中文,那么在调用接口时会出现“签名错误”
  • 在网上找了很多方法,有了如上删除线部分的方法,但是仍然是有问题的,因为支付成功后的凭证里中文是乱码
  • 后来终于在网上各种倒腾,找到了原因,确实是编码问题,但问题不在body是否使用ISO8859-1,而在MD5的加密算法中是否使用UTF-8
  • 所以 md.update(sourceStr.getBytes("UTF-8"));

两个算法的代码如下:
/**
* 生成随机数
* <p>算法参考:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3</p>
* @return 随机数字符串
*/
public static String createNonceStr() {
SecureRandom random = new SecureRandom();
int randomNum = random.nextInt();
return Integer.toString(randomNum);
} /**
* 生成签名,用于在微信支付前,获取预支付时候需要使用的参数sign
* <p>算法参考:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3</p>
* @param params 需要发送的所有数据设置为的Map
* @return 签名sign
*/
public static String createSign(TreeMap<String, String> params) {
String signValue = "";
String stringSignTemp = "";
String stringA = ""; //获得stringA
Set<String> keys = params.keySet();
for (String key : keys) {
stringA += (key + "=" + params.get(key) + "&");
}
stringA = stringA.substring(0, stringA.length() - 1);
//获得stringSignTemp
stringSignTemp = stringA + "&key=" + WeChatAPI.getMerchantKey();
//获得signValue
signValue = encryptByMD5(stringSignTemp).toUpperCase();
log.debug("预支付签名:" + signValue);
return signValue;
} /**
* MD5加密
*
* @param sourceStr
* @return
*/
public static String encryptByMD5(String sourceStr) {
String result = "";
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(sourceStr.getBytes("UTF-8"));
byte b[] = md.digest();
int i;
StringBuffer buf = new StringBuffer("");
for (int offset = 0; offset < b.length; offset++) {
i = b[offset];
if (i < 0)
i += 256;
if (i < 16)
buf.append("0");
buf.append(Integer.toHexString(i));
}
result = buf.toString();
} catch (NoSuchAlgorithmException e) {
System.out.println(e);
}
return result;
}
66
 
1
/**
2
 * 生成随机数
3
 * <p>算法参考:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3</p>
4
 * @return 随机数字符串
5
 */
6
public static String createNonceStr() {
7
    SecureRandom random = new SecureRandom();
8
    int randomNum = random.nextInt();
9
    return Integer.toString(randomNum);
10
}
11

12

13
/**
14
 * 生成签名,用于在微信支付前,获取预支付时候需要使用的参数sign
15
 * <p>算法参考:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3</p>
16
 * @param params 需要发送的所有数据设置为的Map
17
 * @return 签名sign
18
 */
19
public static String createSign(TreeMap<String, String> params) {
20
    String signValue = "";
21
    String stringSignTemp = "";
22
    String stringA = "";
23

24
    //获得stringA
25
    Set<String> keys = params.keySet();
26
    for (String key : keys) {
27
        stringA += (key + "=" + params.get(key) + "&");
28
    }
29
    stringA = stringA.substring(0, stringA.length() - 1);
30
    //获得stringSignTemp
31
    stringSignTemp = stringA + "&key=" + WeChatAPI.getMerchantKey();
32
    //获得signValue
33
    signValue = encryptByMD5(stringSignTemp).toUpperCase();
34
    log.debug("预支付签名:" + signValue);
35
    return signValue;
36
}
37

38

39
/**
40
 * MD5加密
41
 *
42
 * @param sourceStr
43
 * @return
44
 */
45
public static String encryptByMD5(String sourceStr) {
46
    String result = "";
47
    try {
48
        MessageDigest md = MessageDigest.getInstance("MD5");
49
        md.update(sourceStr.getBytes("UTF-8"));
50
        byte b[] = md.digest();
51
        int i;
52
        StringBuffer buf = new StringBuffer("");
53
        for (int offset = 0; offset < b.length; offset++) {
54
            i = b[offset];
55
            if (i < 0)
56
                i += 256;
57
            if (i < 16)
58
                buf.append("0");
59
            buf.append(Integer.toHexString(i));
60
        }
61
        result = buf.toString();
62
    } catch (NoSuchAlgorithmException e) {
63
        System.out.println(e);
64
    }
65
    return result;
66
}

2018.05.22 补充:
以前写的时候没注意过缘由,今天在思考公司内部api调用时简单的安全方式,突然想到了微信的这个。参数字典序后进行md5加密,就是为了防止请求的参数被过程中篡改。你想说,参数我知道,字典序md5加密的签名算法我知道,我改了参数我按同样的方式也修改签名,不是一样的吗?那你可能遗漏了一个东西,就是商户的密钥key,这个东西也是参与进了md5加密算法的,也就是说,第三者修改了参数,但是因为没有密钥,它无法修改签名为正确的签名,篡改后的请求到达服务器就会被发现有问题。

所以有时候密钥不是说一定是用来解密的,单纯用来加密就可以了,这跟加密的算法也是有关系的,md5本身就是不可逆的算法,我只需要比对签名是否一致即可。

2.2.2 调用统一下单接口,获取prepay_id

有了组装好的xml,现在我们直接使用POST方式的请求发送给微信提供的接口就可以了,如果一切顺利,我们会收到微信返回的xml字符串,格式示例如下:
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
<appid><![CDATA[wx2421b1c4370ec43b]]></appid>
<mch_id><![CDATA[10000100]]></mch_id>
<nonce_str><![CDATA[IITRi8Iabbblz1Jc]]></nonce_str>
<openid><![CDATA[oUpF8uMuAJO_M2pxb1Q9zNjWeS6o]]></openid>
<sign><![CDATA[7921E432F65EB8ED0CE9755F0E86D72F]]></sign>
<result_code><![CDATA[SUCCESS]]></result_code>
<prepay_id><![CDATA[wx201411101639507cbf6ffd8b07629950874]]></prepay_id>
<trade_type><![CDATA[JSAPI]]></trade_type>
</xml>
12
 
1
<xml>
2
   <return_code><![CDATA[SUCCESS]]></return_code>
3
   <return_msg><![CDATA[OK]]></return_msg>
4
   <appid><![CDATA[wx2421b1c4370ec43b]]></appid>
5
   <mch_id><![CDATA[10000100]]></mch_id>
6
   <nonce_str><![CDATA[IITRi8Iabbblz1Jc]]></nonce_str>
7
   <openid><![CDATA[oUpF8uMuAJO_M2pxb1Q9zNjWeS6o]]></openid>
8
   <sign><![CDATA[7921E432F65EB8ED0CE9755F0E86D72F]]></sign>
9
   <result_code><![CDATA[SUCCESS]]></result_code>
10
   <prepay_id><![CDATA[wx201411101639507cbf6ffd8b07629950874]]></prepay_id>
11
   <trade_type><![CDATA[JSAPI]]></trade_type>
12
</xml>

其中我们最需要的就是 prepay_id,这个值在后续需要用到。这段过程比较简单,其中提取prepay_id我是用的正则,我直接贴代码好了:

String url = WeChatAPI.getUrl_prePay();
String result = NetUtil.sendRequest(url, "POST", xml); String reg = "<prepay_id><!\\[CDATA\\[(\\w+)\\]\\]></prepay_id>";
Pattern pattern = Pattern.compile(reg);
Matcher matcher = pattern.matcher(result);
String prepayId = "";
while (matcher.find()) {
prepayId = matcher.group(1);
log.debug("预支付ID:" + prepayId);
}
11
 
1
String url = WeChatAPI.getUrl_prePay();
2
String result = NetUtil.sendRequest(url, "POST", xml);
3

4
String reg = "<prepay_id><!\\[CDATA\\[(\\w+)\\]\\]></prepay_id>";
5
Pattern pattern = Pattern.compile(reg);
6
Matcher matcher = pattern.matcher(result);
7
String prepayId = "";
8
while (matcher.find()) {
9
    prepayId = matcher.group(1);
10
    log.debug("预支付ID:" + prepayId);
11
}


2.3 回传参数,调起微信支付JS

2.3.1 回传参数

这时候,已经有了预支付ID,但是后台的处理还没有结束,我们还没有把该有的信息返回给前台。那么前台需要哪些东西呢?《微信支付官方文档 - 微信内H5调起支付》有详细的解释,这里再贴一下:

参数     说明     备注
appId 开发者应用ID,在 “开发 - 基本配置” 查看
timeStamp     时间戳,标准北京时间,秒级(10位数字)
nonceStr     随机字符串 参考算法:《微信支付官方文档 - 安全规范
package     订单详情扩展字符串,其实就是预支付ID 示例: prepay_id=***
signType     签名方式,暂支持MD5
paySign     签名 参考算法:《微信支付官方文档 - 安全规范

有了之前的经验,想必到这里对这些的获取已经没有什么问题了,但是仍然有几个注意的地方:
  • package的值是 “prepay_id=***”,而不是 "***" 的方式(***表示之前获取的prepay_id)
  • timeStamp注意使用标准北京时间,可以使用Calendar设置Locale为CHINA,因为是秒级所以记得除以1000
  • paySign签名要重新生成,算法还是之前的,但是参数需要除自己以外的 appId、timeStamp、nonceStr、package、signType
  • 之前xml中参数appid是小写,这里的appId是大写的I

好了,因为前台接受到参数以后会以JSON的形式发送给微信服务器,所以我们这里后台,直接就把这些参数封装到一个JSONObject中就行了,然后转成JSON的形式发给前台。下面贴一下我的测试代码,签名算法和之前一样,我这里就不重复贴出来了:
Date beijingDate = Calendar.getInstance(Locale.CHINA).getTime();
String nonceStr2 = SignUtil.createNonceStr();
JSONObject json = new JSONObject();
json.put("appId", appId);
json.put("timeStamp", beijingDate.getTime() / 1000);
json.put("nonceStr", nonceStr2);
json.put("package", "prepay_id=" + prepayId);
json.put("signType", "MD5"); TreeMap<String, String> map2 = new TreeMap<String, String>();
map2.put("appId", appId);
map2.put("timeStamp", String.valueOf(beijingDate.getTime() / 1000));
map2.put("nonceStr", nonceStr2);
map2.put("package", "prepay_id=" + prepayId);
map2.put("signType", "MD5");
String paySign = SignUtil.createSign(map2); json.put("paySign", paySign);
String re = json.toJSONString(); AjaxSupport.sendSuccessText(null, re);
21
 
1
Date beijingDate = Calendar.getInstance(Locale.CHINA).getTime();
2
String nonceStr2 = SignUtil.createNonceStr();
3
JSONObject json = new JSONObject();
4
json.put("appId", appId);
5
json.put("timeStamp", beijingDate.getTime() / 1000);
6
json.put("nonceStr", nonceStr2);
7
json.put("package", "prepay_id=" + prepayId);
8
json.put("signType", "MD5");
9

10
TreeMap<String, String> map2 = new TreeMap<String, String>();
11
map2.put("appId", appId);
12
map2.put("timeStamp", String.valueOf(beijingDate.getTime() / 1000));
13
map2.put("nonceStr", nonceStr2);
14
map2.put("package", "prepay_id=" + prepayId);
15
map2.put("signType", "MD5");
16
String paySign = SignUtil.createSign(map2);
17

18
json.put("paySign", paySign);
19
String re = json.toJSONString();
20

21
AjaxSupport.sendSuccessText(null, re);

2.3.2 使用微信内置的JS调起微信支付

前台的调用就很简单了,看下官方给的示例代码:
function onBridgeReady(){
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId":"wx2421bk1c4370c43b", //公众号名称,由商户传入
"timeStamp":"1395712654", //时间戳,自1970年以来的秒数
"nonceStr":"e61463f8efa94090b1f366cccfbbb444", //随机串
"package":"prepay_id=u802345jfgjsdfgsdg888",
"signType":"MD5", //微信签名方式:
"paySign":"70EA570631E4B79628FBCS90534C63FF7FADD89" //微信签名
},
function(res){
if(res.err_msg == "get_brand_wcpay_request:ok" ) {}  
// 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
}
);
}
if (typeof WeixinJSBridge == "undefined"){
if( document.addEventListener ){
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
}else if (document.attachEvent){
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
}else{
onBridgeReady();
}
26
 
1
function onBridgeReady(){
2
   WeixinJSBridge.invoke(
3
       'getBrandWCPayRequest', {
4
           "appId":"wx2421bk1c4370c43b",     //公众号名称,由商户传入     
5
           "timeStamp":"1395712654",         //时间戳,自1970年以来的秒数     
6
           "nonceStr":"e61463f8efa94090b1f366cccfbbb444", //随机串     
7
           "package":"prepay_id=u802345jfgjsdfgsdg888",     
8
           "signType":"MD5",         //微信签名方式:     
9
           "paySign":"70EA570631E4B79628FBCS90534C63FF7FADD89" //微信签名 
10
       },
11
       function(res){     
12
           if(res.err_msg == "get_brand_wcpay_request:ok" ) {}     
13
           // 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。 
14
       }
15
   ); 
16
}
17
if (typeof WeixinJSBridge == "undefined"){
18
   if( document.addEventListener ){
19
       document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
20
   }else if (document.attachEvent){
21
       document.attachEvent('WeixinJSBridgeReady', onBridgeReady); 
22
       document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
23
   }
24
}else{
25
   onBridgeReady();
26
}

使用时直接替换掉invoke方法中的参数即可,实际上如果后台直接是传递的JSON字符串到前台,可以直接解析为JS对象作为参数,下面贴我自己的代码:
$().invoke("/pay/do/pay.q", null, function (re) {
var result = JSON.parse(re);
function onBridgeReady(){
WeixinJSBridge.invoke(
'getBrandWCPayRequest', result, function(res){
alert(JSON.stringify(res));
if(res.err_msg == "get_brand_wcpay_request:ok" ) {
//doit 这里处理支付成功后的逻辑,通常为页面跳转
}
}
);
} if (typeof WeixinJSBridge == "undefined"){
if( document.addEventListener ){
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
}else if (document.attachEvent){
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
}else{
onBridgeReady();
} });
25
 
1
$().invoke("/pay/do/pay.q", null, function (re) {
2
    var result = JSON.parse(re);
3
    function onBridgeReady(){
4
       WeixinJSBridge.invoke(
5
           'getBrandWCPayRequest', result, function(res){
6
               alert(JSON.stringify(res));
7
               if(res.err_msg == "get_brand_wcpay_request:ok" ) {
8
                   //doit 这里处理支付成功后的逻辑,通常为页面跳转
9
               }
10
           }
11
       );
12
    }
13

14
    if (typeof WeixinJSBridge == "undefined"){
15
       if( document.addEventListener ){
16
           document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
17
       }else if (document.attachEvent){
18
           document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
19
           document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
20
       }
21
    }else{
22
       onBridgeReady();
23
    }
24

25
});

这里还有个坑,是iOS和Android系统不同导致的,如上代码:
  • 如果你在 var result = JSON.parse(re); 之前再添加一个用于debug的输出语句  alert(re);
  • 你可以看到传过来的各项参数,其中timeStamp的值是没有双引号的,这会导致在iOS中支付出现错误,提示缺少timeStamp参数

   
 
所以为了兼容,必须要将这个转换成字符串,带上双引号:

$().invoke("/pay/do/pay.q", null, function (re) {
var result = JSON.parse(re);
result['timeStamp'] = result['timeStamp'] + "";
function onBridgeReady(){
WeixinJSBridge.invoke(
'getBrandWCPayRequest', result, function(res){
alert(JSON.stringify(res));
if(res.err_msg == "get_brand_wcpay_request:ok" ) {
//doit 这里处理支付成功后的逻辑,通常为页面跳转
}
}
);
}
if (typeof WeixinJSBridge == "undefined"){
if( document.addEventListener ){
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
}else if (document.attachEvent){
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
}else{
onBridgeReady();
}
});
24
 
1
$().invoke("/pay/do/pay.q", null, function (re) {
2
    var result = JSON.parse(re);
3
    result['timeStamp'] = result['timeStamp'] + "";
4
    function onBridgeReady(){
5
       WeixinJSBridge.invoke(
6
           'getBrandWCPayRequest', result, function(res){
7
               alert(JSON.stringify(res));
8
               if(res.err_msg == "get_brand_wcpay_request:ok" ) {
9
                   //doit 这里处理支付成功后的逻辑,通常为页面跳转
10
               }
11
           }
12
       );
13
    }
14
    if (typeof WeixinJSBridge == "undefined"){
15
       if( document.addEventListener ){
16
           document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
17
       }else if (document.attachEvent){
18
           document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
19
           document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
20
       }
21
    }else{
22
       onBridgeReady();
23
    }
24
});


另外,在这个页面调试有个小技巧,将微信回调的JS对象序列化为JSON字符串,进行弹窗显示:alert(JSON.stringify(res));
 

2.4 校验信息的正确性

实际上在完成上面的步骤以后,已经可以进行微信支付了。这最后一步主要是为了确认支付信息的正确性,以及传递给我们本次支付的一些信息,以便业务处理。

支付成功后,微信会将本次支付的相关信息,以流的方式发送给我们指定的url地址,而我们指定的url地址,就是第一次组装xml时 <notify_url> 中填写的地址,下面我们可以先回顾一下:
...
String notifyUrl = "http://k169710n05.51mypc.cn/pay/do/afterPaySuccess.q";
...
String xml = "<xml>" +
"<appid>" + appId + "</appid>" +
"<body>" + body +"</body>" +
"<device_info>WEB</device_info>" +
"<mch_id>" + merchantId + "</mch_id>" +
"<nonce_str>" + nonceStr1 + "</nonce_str>" +
"<notify_url>" + notifyUrl +"</notify_url>" +
"<openid>" + openId + "</openid>" +
"<out_trade_no>" + tradeNo + "</out_trade_no>" +
"<total_fee>" + totalFee + "</total_fee>" +
"<trade_type>JSAPI</trade_type>" +
"<sign>" + sign + "</sign>" +
"</xml>";
16
 
1
...
2
String notifyUrl = "http://k169710n05.51mypc.cn/pay/do/afterPaySuccess.q";
3
...
4
String xml = "<xml>" +
5
                "<appid>" + appId + "</appid>" +
6
                "<body>" + body +"</body>" +
7
                "<device_info>WEB</device_info>" +
8
                "<mch_id>" + merchantId + "</mch_id>" +
9
                "<nonce_str>" + nonceStr1 + "</nonce_str>" +
10
                "<notify_url>" + notifyUrl +"</notify_url>" +
11
                "<openid>" + openId + "</openid>" +
12
                "<out_trade_no>" + tradeNo + "</out_trade_no>" +
13
                "<total_fee>" + totalFee + "</total_fee>" +
14
                "<trade_type>JSAPI</trade_type>" +
15
                "<sign>" + sign + "</sign>" +
16
             "</xml>";

而我们要做的,就是接受到这些信息后,进行处理,并对微信服务器做出应答。如果微信收到商户的应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。详情请参考《微信支付官方文档 - 支付结果通知

需要做三件事
  • 解析微信发来的信息,通过重新签名的方式验证信息的正确性,确认信息是否是微信所发
  • return_code和result_code都是SUCCESS的话,处理商户自己的业务逻辑
  • 应答微信,告诉它说我们收到信息了,不用再发了(如果不进行应答,则微信服务器会通过一定的策略定期重新发起通知)

过程也很简单,将微信发来的流信息解析出来之后,再次调用之前的签名算法,用计算出来的算法,和微信发来的xml中的签名sign进行对比,如果相同,则说明是微信返回的通知,响应给微信即可。

注意:验证调用返回或微信主动通知签名时,传送的sign参数不参与签名,而是将生成的签名与该sign值作校验。也就是说,微信发来的xml中包含元素sign,该元素内容不参与签名算法之中,而是和最后算法的结果进行比较的,所以传参进行算法的时候不用加入sign值。

好了,现在我们先看下微信发回来的流信息是什么,实际上文档里有说明,就是个xml,我们看下官方的示例:
<xml>
<appid><![CDATA[wx2421b1c4370ec43b]]></appid>
<attach><![CDATA[支付测试]]></attach>
<bank_type><![CDATA[CFT]]></bank_type>
<fee_type><![CDATA[CNY]]></fee_type>
<is_subscribe><![CDATA[Y]]></is_subscribe>
<mch_id><![CDATA[10000100]]></mch_id>
<nonce_str><![CDATA[5d2b6c2a8db53831f7eda20af46e531c]]></nonce_str>
<openid><![CDATA[oUpF8uMEb4qRXf22hE3X68TekukE]]></openid>
<out_trade_no><![CDATA[1409811653]]></out_trade_no>
<result_code><![CDATA[SUCCESS]]></result_code>
<return_code><![CDATA[SUCCESS]]></return_code>
<sign><![CDATA[B552ED6B279343CB493C5DD0D78AB241]]></sign>
<sub_mch_id><![CDATA[10000100]]></sub_mch_id>
<time_end><![CDATA[20140903131540]]></time_end>
<total_fee>1</total_fee>
<trade_type><![CDATA[JSAPI]]></trade_type>
<transaction_id><![CDATA[1004400740201409030005092168]]></transaction_id>
</xml>
19
 
1
<xml>
2
  <appid><![CDATA[wx2421b1c4370ec43b]]></appid>
3
  <attach><![CDATA[支付测试]]></attach>
4
  <bank_type><![CDATA[CFT]]></bank_type>
5
  <fee_type><![CDATA[CNY]]></fee_type>
6
  <is_subscribe><![CDATA[Y]]></is_subscribe>
7
  <mch_id><![CDATA[10000100]]></mch_id>
8
  <nonce_str><![CDATA[5d2b6c2a8db53831f7eda20af46e531c]]></nonce_str>
9
  <openid><![CDATA[oUpF8uMEb4qRXf22hE3X68TekukE]]></openid>
10
  <out_trade_no><![CDATA[1409811653]]></out_trade_no>
11
  <result_code><![CDATA[SUCCESS]]></result_code>
12
  <return_code><![CDATA[SUCCESS]]></return_code>
13
  <sign><![CDATA[B552ED6B279343CB493C5DD0D78AB241]]></sign>
14
  <sub_mch_id><![CDATA[10000100]]></sub_mch_id>
15
  <time_end><![CDATA[20140903131540]]></time_end>
16
  <total_fee>1</total_fee>
17
  <trade_type><![CDATA[JSAPI]]></trade_type>
18
  <transaction_id><![CDATA[1004400740201409030005092168]]></transaction_id>
19
</xml>

其中除了sign的值,其他的值需要做成集合进行签名算法,然后结果和sign值对比,相同的话,给微信一个应答,应答的格式官方也给出了示例,如下:
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
</xml>
4
 
1
<xml>
2
  <return_code><![CDATA[SUCCESS]]></return_code>
3
  <return_msg><![CDATA[OK]]></return_msg>
4
</xml>

总之,这一部分还是很简单的,就直接上我的代码了:
/**
* 支付成功后的处理
* <p>微信支付成功后,对微信返回的信息进行校验</p>
* @return
*/
public String afterPaySuccess() {
HttpServletRequest request = ServletActionContext.getRequest();
HttpServletResponse response = ServletActionContext.getResponse(); TreeMap<String, String> map = new TreeMap<String, String>();
try {
//解析xml,存入map
InputStream inputStream = request.getInputStream();
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(inputStream);
Element rootElement = document.getRootElement();
List<Element> elements = rootElement.elements(); String reg = "<!\\[CDATA\\[(.+)\\]\\]>";
Pattern pattern = Pattern.compile(reg);
for (Element element : elements) {
String key = element.getName();
String value = element.getText();
Matcher matcher = pattern.matcher(value);
while (matcher.find()) {
value = matcher.group(1);
}
map.put(key, value);
} //如果微信结果通知为失败
if ("FAIL".equals(map.get("return_code"))) {
log.debug(map.get("return_msg"));
return NONE;
} //doit 处理商户业务逻辑 //签名对比,应答微信服务器
String signFromWechat = map.get("sign");
map.remove("sign");
String sign = SignUtil.createSign(map);
if (sign.equals(signFromWechat)) {
String responseXml = "<xml>" +
"<return_code><![CDATA[SUCCESS]]></return_code>" +
"<return_msg><![CDATA[OK]]></return_msg>" +
"</xml>";
response.getWriter().write(responseXml);
} } catch (IOException e) {
e.printStackTrace();
} catch (DocumentException e) {
e.printStackTrace();
} return NONE;
}
x
 
1
/**
2
 * 支付成功后的处理
3
 * <p>微信支付成功后,对微信返回的信息进行校验</p>
4
 * @return
5
 */
6
public String afterPaySuccess() {
7
    HttpServletRequest request = ServletActionContext.getRequest();
8
    HttpServletResponse response = ServletActionContext.getResponse();
9

10
    TreeMap<String, String> map = new TreeMap<String, String>();
11
    try {
12
        //解析xml,存入map
13
        InputStream inputStream = request.getInputStream();
14
        SAXReader saxReader = new SAXReader();
15
        Document document = saxReader.read(inputStream);
16
        Element rootElement = document.getRootElement();
17
        List<Element> elements = rootElement.elements();
18

19
        String reg = "<!\\[CDATA\\[(.+)\\]\\]>";
20
        Pattern pattern = Pattern.compile(reg);
21
        for (Element element : elements) {
22
            String key = element.getName();
23
            String value = element.getText();
24
            Matcher matcher = pattern.matcher(value);
25
            while (matcher.find()) {
26
                value = matcher.group(1);
27
            }
28
            map.put(key, value);
29
        }
30

31
        //如果微信结果通知为失败
32
        if ("FAIL".equals(map.get("return_code"))) {
33
            log.debug(map.get("return_msg"));
34
            return NONE;
35
        }
36

37
        //doit 处理商户业务逻辑
38

39
        //签名对比,应答微信服务器
40
        String signFromWechat = map.get("sign");
41
        map.remove("sign");
42
        String sign = SignUtil.createSign(map);
43
        if (sign.equals(signFromWechat)) {
44
            String responseXml = "<xml>" +
45
                                    "<return_code><![CDATA[SUCCESS]]></return_code>" +
46
                                    "<return_msg><![CDATA[OK]]></return_msg>" +
47
                                 "</xml>";
48
            response.getWriter().write(responseXml);
49
        }
50

51
    } catch (IOException e) {
52
        e.printStackTrace();
53
    } catch (DocumentException e) {
54
        e.printStackTrace();
55
    }
56

57
    return NONE;
58
}

另外,如果在执行支付流程中,有部分数据希望能放在支付完成后再处理,可以在组装xml的时候放置在attach标签中;然后在支付完成后微信发送来的xml中,会将原数据在此返回。需要注意的是,该attach有字符串的长度限制(详见文档),所以试图直接在支付处理时直接把某个类的JSON格式放进来留做事后处理,是会出错的(我就是这样踩了坑),所以用来传递一些核心数据就行了。

再另外,对于最后这部分,看看微信推荐我们的做法是:当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。另,商户系统对于支付结果通知的内容一定要做签名验证,并校验返回的订单金额是否与商户侧的订单金额一致,防止数据泄漏导致出现“假通知”,造成资金损失。


3、参考链接




附件列表

微信公众号开发 [05] 微信支付功能开发(网页JSAPI调用)的更多相关文章

  1. weixin-java-mp集成微信公众号自带客服功能

    电脑端登录公众号管理后台,[添加功能插件]开通客服功能,输入"人工客服"接入客服热线 底部有我的微信二维码,如有问题,可加好友进行技术交流! ​ ​ ​ ​ ​ ​ ​ weixi ...

  2. 在微信框架模块中,基于Vue&Element前端的微信公众号和企业微信的用户绑定

    在一个和微信相关的业务管理系统,我们有时候需要和用户的微信账号信息进行绑定,如对公众号.企业微信等账号绑定特定的系统用户,可以进行扫码登录.微信信息发送等操作,用户的绑定主要就是记录公众号用户的ope ...

  3. 微信公众号中的支付宝支付与微信支付 && 支付宝支付问题(微信bug)

    一般,在微信公众号中的商城都是需要支持微信支付和支付宝支付的,当然,较大的公司对于鹅厂和阿里的站队就不说了,所以这里简单记录一下支付宝支付和微信支付的主要流程.说是简单介绍,这是因为确实不难,因为前端 ...

  4. Vue — 微信公众号内置h5支付相关

    首先,在公众号后台配置h5页面地址 开发流程 1.通过配置h5地址,获取code.再通过code,获取openid getOpenid(){ let url = 'https://open.weixi ...

  5. 微信公众号内唤起h5支付详解

    1.调用统一下单的接口URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder 2.调用统一下单必传参数: appid:需要进行支付功能的公众号的app ...

  6. vue微信公众号、H5微信支付

    1.H5微信支付 后台会返回一个URL,前端直接跳转就OK(需要你传给后台一个ip,必须保证在同一域名下) 使用window.location.href =res.data;进行页面跳转到支付界面(r ...

  7. 微信公众号与APP微信第三方登录账号打通

    一个项目同时开发了APP和微信服务号,需要做到APP和微信服务号的账号互通同步,也就是说一个账号在2个地方都可以用,当然这个前提是保证你公司自己的服务器的数据库用的是同一套. 为保证用户数据的唯一性, ...

  8. 用wxpy管理微信公众号,并利用微信获取自己的开源数据。

    之前了解到itchat 乃至于 wxpy时 是利用tuling聊天机器人的接口.调用接口并保存双方的问答结果可以作为自己的问答词库的一个数据库累计.这些数据可以用于自己训练. 而最近希望获取一些语音资 ...

  9. 【微信公众号】将微信公众号消息里的FromUserName即OpenID转成UnionID

    最近在调试微信公众号开发者模式,处理公众号消息,收到如下回调消息内容 <xml><ToUserName><![CDATA[gh_29********21]]>< ...

  10. [Python爬虫] 之十五:Selenium +phantomjs根据微信公众号抓取微信文章

    借助搜索微信搜索引擎进行抓取 抓取过程 1.首先在搜狗的微信搜索页面测试一下,这样能够让我们的思路更加清晰 在搜索引擎上使用微信公众号英文名进行“搜公众号”操作(因为公众号英文名是公众号唯一的,而中文 ...

随机推荐

  1. Django 系列博客(十六)

    Django 系列博客(十六) 前言 本篇博客介绍 Django 的 forms 组件. 基本属性介绍 创建 forms 类时,主要涉及到字段和插件,字段用于对用户请求数据的验证,插件用于自动生成 h ...

  2. 使用LR编写HTTP协议Json报文格式接口脚本实战

    最近在做HTTP协议接口压测时,遇到一些编写脚本方面的问题,在这里总结记录下,以便以后温习,也希望能帮助到和我有同样困惑的朋友吧. //实战代码如下所示:Action() { lr_start_tra ...

  3. 跨站脚本攻击xss学习

    0.认识跨站脚本 举一个跨站脚本的简单例子. 假设一个页面将用户输入的参数直接显示到页面之中.(比如有如下代码) 在实际的浏览器中,在param中提交的参数正常会展示到页面之中.比如输入下面的URL: ...

  4. C# 通过进程名/进程Id 操作窗口/程序

    1. 判断窗口是否存在 private bool IsWindowExist(IntPtr handle) { ) != IntPtr.Zero) && IsWindowVisible ...

  5. windows server 2012/2016 设置多用户远程桌面(转载)

    方案一: 该方案只能满足两个人同时远程. 方案二: https://blog.csdn.net/flyingshuai/article/details/77869279

  6. 设计模式总结(Java)—— 观察者模式

    概述 它用于建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应作出反应.在观察者模式中,发生改变的对象称为观察目标,而被通知的对象称为观察者,一个观察目标可以对应多 ...

  7. 手机端input[type=date]的placeholder不起作用

    <div class="input clearfix"> <label class="fl">起始日期</label> &l ...

  8. 程序员50题(JS版本)(五)

    程序21:有一分数序列:2/1,3/2,5/3,8/5,13/8,21/13...求出这个数列的前20项之和. var arr=[]; var count=20; for(var i=0;i<= ...

  9. 弹框在UC浏览器或者Android机器上会被顶上去

    弹框在UC浏览器或者Android机器上会被顶上去 可以通过监听resize事件 this.height = $(document).height(); window.addEventListener ...

  10. Xamarin for Visual Studio下载后的文件路径

    Xamarin for Visual Studio的下载很纠结,在官网上不知道如何下载?现在找到一个办法:可以先在网上找一个低版本的之后安装,然后利用VS更新.利用VS更新这里也遇到了问题,下载成功之 ...