首先必须要吐槽一下,可能真的是我天资愚钝,看不懂微信的文档,导致这几天在做微信支付的时候踩了很多的坑,为了避免以后再次出现这样的情况,忍痛回忆一下这几天的经历。
先来说一下需要做的准备工作吧
- 申请开通微信H5支付及公众号支付(‘微信商户平台’->产品中心->支付产品)
- 设置网站授权目录(同上->开发设置)
这里需要注意的是,H5支付设置当前域名即可,公众号支付需要设置为支付页面所在目录(比如支付页面路径为xxxx.com/pay,H5设置xxxx.com即可,公众号需要设置xxxx.com/pay) - 设置js接口安全域名和授权目录(‘微信公众平台’->接口权限)
设置为域名即可(这里需要注意www的问题,需要保持一致,如果设置位xxx.com,那么在www.xxx.com 访问的时候,微信会认为没有权限),之后把微信提供的文本文件放在服务器根目录 - 拿到公众号的appid和密钥
- 商户号和商户密钥(‘微信商户平台’->账户设置->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支付需要的开发步骤要简单得多,不知道微信为什么要这么折腾自家浏览器。
第一步 在后台对微信统一下单
下单参数详见‘微信统一下单文档’
这里唯一需要注意就是签名算法:
- 将所有非空参数以URL键值对的方式,按照参数名ASCII码从小到大排序,拼接为字符串,注意大小写
- 将字符串尾部拼接
&key=商户密钥
- 将字符串使用MD5加密后转为大写
举个栗子:1
2
3
4
5
6
7
8
9
10appid: 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
8public 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
9onWechatPay () {
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-sdk
的chooseWXPay
方法来发起支付。
第一步 网页授权获取openid
使用公众号支付,即trade_type为JSAPI
时,统一下单的openid参数是必填的,所以我们首先要做的就是通过微信网页授权拿到用户在公众号对应appid下的唯一标识openid。
在进行这一步之前,首先需要检查授权回调域名是否设置正确(见上文准备工作),确保无误后,在前台通过页面跳转拿到授权,具体可以查看‘微信网页授权
‘1
2
3
4
5
6
7onWechatPay () {
// 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
8wx.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
41. 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
9public 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
18import 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
13wx.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
3wx.ready(function () {
wx.chooseWXPay(...)
})
由于wx.config的进程是异步的,只有在ready方法中才能保证config配置完成
第五步 处理通知
同上,可以和H5支付使用相同的通知地址
最后总结一下, 嗯, 一共3个api, 向微信请求5次, 3个签名
- 网页授权, 前台直接get请求
- 获取openid, 后台请求
- 统一下单, 后台统一下单, 包含第一个签名
- 获取config签名, 后台请求获取access_token, 再请求获取jsapi_ticket, 第二个签名
- 获取支付签名, 第三个签名
分享一下这部分的源码‘链接’,密码: kugp