微信H5支付&公众号支付大型攻略

首先必须要吐槽一下,可能真的是我天资愚钝,看不懂微信的文档,导致这几天在做微信支付的时候踩了很多的坑,为了避免以后再次出现这样的情况,忍痛回忆一下这几天的经历。

先来说一下需要做的准备工作吧

  1. 申请开通微信H5支付及公众号支付(‘微信商户平台’->产品中心->支付产品)
  2. 设置网站授权目录(同上->开发设置)
    这里需要注意的是,H5支付设置当前域名即可,公众号支付需要设置为支付页面所在目录(比如支付页面路径为xxxx.com/pay,H5设置xxxx.com即可,公众号需要设置xxxx.com/pay)
  3. 设置js接口安全域名和授权目录(‘微信公众平台’->接口权限)
    设置为域名即可(这里需要注意www的问题,需要保持一致,如果设置位xxx.com,那么在www.xxx.com 访问的时候,微信会认为没有权限),之后把微信提供的文本文件放在服务器根目录
  4. 拿到公众号的appid和密钥
  5. 商户号和商户密钥(‘微信商户平台’->账户设置->API安全->密钥设置)

ok,准备工作完成之后,就可以开始我们的大型攻(cai)略(keng)了, 先来介绍一下项目需求,这次做的是一个扫码在线点餐的网页,由于是在浏览器使用微信支付,最后在集成支付的时候,需要用到两种支付方式:微信外浏览器使用H5支付(微信内使用会提示,请在微信外浏览器打开),微信内浏览器使用公众号支付, 那么就需要用到一个判断方法。

1
2
3
4
5
// 判断是否为微信浏览器
export function isWechat() {
let userAgent = window.navigator.userAgent.toLowerCase();
return userAgent.indexOf('micromessenger') !== -1;
}

H5支付

相比公众号支付,H5支付需要的开发步骤要简单得多,不知道微信为什么要这么折腾自家浏览器。

第一步 在后台对微信统一下单

下单参数详见‘微信统一下单文档’
这里唯一需要注意就是签名算法:

  1. 将所有非空参数以URL键值对的方式,按照参数名ASCII码从小到大排序,拼接为字符串,注意大小写
  2. 将字符串尾部拼接&key=商户密钥
  3. 将字符串使用MD5加密后转为大写

举个栗子:

1
2
3
4
5
6
7
8
9
10
appid: wxqwer123456
mch_id: 10086
body: test

// 按字典序排序参数
appid=wxqwer123456&body=test&mch_id=10086
// 添加key
appid=wxqwer123456&body=test&mch_id=10086&key=key
// MD5加密转大写
F5D442C19378535AB235223241D76484

签名代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 生成签名,参数为数组
public function MakeSign($data)
{
//按字典序排序参数
ksort($data);
$string = $this->ToUrlParams($data);
//在string后加入key
$string = $string . "&key=" . $this->key; // 商家密钥
//MD5加密
$string = md5($string);
//所有字符转为大写
$sign = strtoupper($string);

return $sign;
}

// 格式化参数格式化成url参数
public function ToUrlParams($data)
{
$buff = "";
foreach ($data as $k => $v)
{
if($k != "sign" && $v != "" && !is_array($v)){
$buff .= $k . "=" . $v . "&";
}
}

$buff = trim($buff, "&");
return $buff;
}

对于签名算法的验证可以使用‘微信支付接口签名校验工具’

之后需要用post方式将参数以xml的形式提交到微信统一下单接口https://api.mch.weixin.qq.com/pay/unifiedorder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 将数组转换为xml
public function ToXml()
{
$xml = "<xml>";
foreach ($this->values as $key=>$val)
{
if (is_numeric($val)){
$xml.="<".$key.">".$val."</".$key.">";
}else{
$xml.="<".$key."><![CDATA[".$val."]]></".$key.">";
}
}
$xml.="</xml>";
return $xml;
}

当然,返回的数据也是xml格式,我们需要对其进行解析

1
2
3
4
5
6
7
8
public function FromXml($xml)
{
//将XML转为array
//禁止引用外部xml实体
libxml_disable_entity_loader(true);
$this->values = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
return $this->values;
}

第二步 发起支付

之后在下单成功后将微信返回的mweb_url支付跳转链接返回给前台,前台访问链接即可唤起微信客户端,中间页会先进行权限的校验和安全性检查,‘常见错误’
这里可以通过给mweb_url添加redirect_url参数来设置回调页面
虽然微信文档上说明的是默认的回调地址为支付发起的页面,但是经过实践表明对于SPA单页应用的识别很不友好,所以还是自己额外设置一下吧。

1
mweb_url += '&redirect_url=' + encodeURIComponent(redirect_url)

一定要记得对url使用encodeURIComponent进行转码

所以说对于H5支付,前台需要进行的操作十分简单,请求后台接口后打开url即可

1
2
3
4
5
6
7
8
9
onWechatPay () {
this.$http.post('order/wxpay/create', { // 后台下单api
order: this.order
}).then(res => {
let url = res.data.data.mweb_url;
url += '&redirect_url=' + encodeURIComponent(redirect_url); // redirect_url为回调地址
window.location.href = url;
})
}

第三步 处理通知

接下来需要去处理微信支付成功后的通知,在统一下单时设置的notify_url,就是接收微信支付异步通知回调地址,微信会向该地址发送xml,类似

1
2
3
4
5
6
<xml>
<appid>wx123456</appid>
<body>H5支付测试</body>
<out_trade_no>10086</out_trade_no>
……
</xml>

我们需要将得到的xml进行解析,转换为可用的数据,方法在上文有提到,拿到数据之后就可以为所欲为了,到这里H5支付的全部流程就算完成了

公众号支付

这真的是个深坑,深不见底的深坑,相比H5支付直接使用链接打开,公众号支付首先多了一个openid的授权,而且需要使用微信浏览器自带的WeixinJSBridge或者weixin-js-sdk,虽然前者是微信官方文档上推荐的用法,但是实际用起来效果并不好,也可能是我的使用方法有问题,这里我选择使用weixin-js-sdkchooseWXPay方法来发起支付。

第一步 网页授权获取openid

使用公众号支付,即trade_type为JSAPI时,统一下单的openid参数是必填的,所以我们首先要做的就是通过微信网页授权拿到用户在公众号对应appid下的唯一标识openid。
在进行这一步之前,首先需要检查授权回调域名是否设置正确(见上文准备工作),确保无误后,在前台通过页面跳转拿到授权,具体可以查看‘微信网页授权

1
2
3
4
5
6
7
onWechatPay () {
// appId: 公众号appid
// redirect_uri: 授权回调地址
// state: 需要传递的参数
// scope: snsapi_base 只获取openid,页面会直接跳转,snsapi_userinfo 会弹出授权页面,获取用户信息
let url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirect_uri}&response_type=code&scope=snsapi_base&state=${state}#wechat_redirect`;
}

授权完成后,页面将会来到的授权的回调地址,并且微信会将参数附加到地址上

1
redirect_uri/?code=CODE&state=STATE

这里的code是之后用来获取openid的凭据,state是之前自己附加的参数

关于授权回调地址,建议新建一个页面来作为发起微信支付的中间页,避免支付页面的逻辑过于复杂

在回调页面拿到code值之后,需要再次通过一个get请求拿到openid的值,由于请求参数包含公众号的appsecret,建议这一步操作放在后台来完成

1
2
3
4
5
6
7
8
9
10
11
$code = $request['code'];

// get_request是自己封装的发起get请求的方法,这里就不介绍了
$result = get_request('https://api.weixin.qq.com/sns/oauth2/access_token', array(
'appid' => $wechat['appid'],
'secret' => $wechat['appsecret'],
'code' => $code,
'grant_type' => 'authorization_code',
));

return json_decode($result, true);

前台传递code值访问后台api,这样在得到了用户唯一标识openid后,就可以进行下单操作了

第二步 统一下单

公众号支付的统一下单api完全可以复用之前H5支付的,增加了openid的参数由前台传递,签名方式也相同。

第三步 配置sdk

关于weixin-js-sdk,具体可以查看‘说明文档’->微信网页开发->微信JS-SDK说明文档。
接下来介绍一下config时需要用到的参数,建议由后台下单api返回

1
2
3
4
5
6
7
8
wx.config({
debug: false, // debug 模式,开启后pc端以log,移动端以alert的形式提示信息
appId: appid, // appid,不解释
timestamp: timeStamp, // 10位时间戳,字符串格式,注意是10位,表示的是秒数而不是毫秒数,是字符串不是数字,小写!小写!小写!
nonceStr: nonce_str, // 随机字符串
signature: sign, // 签名,重点,下文会详细介绍
jsApiList: ['chooseWXPay'] // 需要用到的api列表
});

又是签名,这个签名非常关键,我可是在这里卡了整整一天,这里的签名需要用到的参数有

1
2
3
4
1. noncestr  // 注意 小写!小写!小写!config时是驼峰,这里是小写,而且要值保持一致
2. timestamp // 注意保持一致,字符串格式
3. url // 当前发起请求的url,需要在商家后台设置公众号授权域名至页面所在目录,而且对于spa单页应用非常不友好,官方文档上说明需要#号之前路径,但我实践发现并不行,需要完整路径才能签名成功
4. jsapi_ticket // 公众号用于调用微信JS接口的临时票据,需要一个get请求获取到access_token,再一个get请求拿到

先来介绍一下如何获取到这个jsapi_ticket吧,我选择放在后台来请求
这里需要注意的是,由于access_token的唯一性,在获取access_token之后,之前获取到的都会失效,所以需要把access_token储存在数据库,在7200s的有效期内,访问数据库取值,而不是重复请求。
创建后台api,接受前台传递的timeStamp,nonceStr和url来获取签名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$timeStamp = $request['timeStamp'];
$nonceStr = $request['nonceStr'];
$url = $request['url'];

// 获取access_token
if ('判断是否已有未过期的access_token') { //
$access_token = 'ss';
}else {
$result = get_request('https://api.weixin.qq.com/cgi-bin/token', array(
'appid' => 'appid',
'secret' => 'secret',
'grant_type' => 'client_credential',
));
$result = json_decode($result, true);
$access_token = $result['access_token'];
// 入库
}

//获取jsapi_ticket
$result = get_request('https://api.weixin.qq.com/cgi-bin/ticket/getticket', array(
'access_token' => $access_token,
'type' => 'jsapi',
));
$result = json_decode($result, true);
$ticket = $result['ticket'];

$data = [
'jsapi_ticket' => $ticket,
'timestamp' => $timeStamp,
'noncestr' => $nonceStr,
'url' => urldecode($url), // 对于url前台转码,后台解码
];

return $this->makeSign($data, false);

比较坑的一点在于,这里的签名算法跟下单的签名算法不一样,使用sha1加密,而不是MD5加密,并且不需要添加key。

1
2
3
4
5
6
7
8
9
public function MakeSign($data)
{
//按字典序排序参数
ksort($data);
$string = $this->ToUrlParams($data);
// sha1加密
$sign = sha1($string);
return $sign;
}

前台访问api配置sdk即可, 需要在发起支付的页面调用config方法(url的变化会导致config失效)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import wx from 'weixin-js-sdk';

// timeStamp我是由前台生成的 timeStamp = parseInt(new Date().getTime() / 1000).toString()
async function wechatConfig (wechat, timeStamp) { // wechat 为统一下单返回的数据,
let result = await Vue.http.post('webpay/sign', {
timeStamp: timeStamp,
nonceStr: wechat.nonce_str,
url: encodeURIComponent(document.URL)
});
wx.config({
debug: false,
appId: wechat.appid,
timestamp: timeStamp,
nonceStr: wechat.nonce_str,
signature: result.data.data,
jsApiList: ['chooseWXPay']
});
}

常见的错误见‘官网文档’->微信网页开发->微信JS-SDK说明文档->附录5 常见错误,特别需要注意参数的大小写以及url

由于异步请求较多,建议使用 async await的方式

第四步 发起支付

在sdk配置完成之后, 就可以使用其chooseWXPay方法来发起支付了, 先来介绍一下参数

1
2
3
4
5
6
7
8
9
10
11
12
13
wx.chooseWXPay({
timestamp: timeStamp, // 小写,其他同上
nonceStr: nonce_str, // 不解释
package: 'prepay_id=' + prepay_id, // prepay_id由统一下单接口返回的,注意提交格式
signType: 'MD5', // 默认为'SHA1',新版支付需要使用'MD5'
paySign: sign, // 第三个签名了...下面介绍
success: function () {
// 支付成功回调
},
cancel: function() {
// 取消支付回调
}
});

其他的参数就不多说了,主要讲一下这个签名吧,第三个签名了,无力吐槽, 签名方式和统一下单相同,需要添加商户key使用MD5加密,依旧放在后台完成

1
2
3
4
5
6
7
8
9
10
11
12
13
$prepayid = $request['prepayid'];
$timeStamp = $request['timeStamp'];
$nonceStr = $request['nonceStr'];

$data = [
'appId' => $wechat['appid'],
'timeStamp' => $timeStamp, // 注意参数为驼峰大写
'nonceStr' => $nonceStr,
'package' => 'prepay_id='. $prepayid, // 注意了,和前台一样,需要添加prepay_id=
'signType' => 'MD5'
];

return $this->makeSign($data)

参数配置完成之后,调用方法,即可发起支付,还有一点要注意的是,需要在ready方法中触发

1
2
3
wx.ready(function () {
wx.chooseWXPay(...)
})

由于wx.config的进程是异步的,只有在ready方法中才能保证config配置完成

第五步 处理通知

同上,可以和H5支付使用相同的通知地址

最后总结一下, 嗯, 一共3个api, 向微信请求5次, 3个签名

  1. 网页授权, 前台直接get请求
  2. 获取openid, 后台请求
  3. 统一下单, 后台统一下单, 包含第一个签名
  4. 获取config签名, 后台请求获取access_token, 再请求获取jsapi_ticket, 第二个签名
  5. 获取支付签名, 第三个签名

分享一下这部分的源码‘链接’,密码: kugp

文章目录
  1. 1. H5支付
    1. 1.1. 第一步 在后台对微信统一下单
    2. 1.2. 第二步 发起支付
    3. 1.3. 第三步 处理通知
  2. 2. 公众号支付
    1. 2.1. 第一步 网页授权获取openid
    2. 2.2. 第二步 统一下单
    3. 2.3. 第三步 配置sdk
    4. 2.4. 第四步 发起支付
    5. 2.5. 第五步 处理通知
|