先上图
场景
本例的代付场景是,开发一款APP应用,APP中具有支付能力,按照微信
支付对接要求已经完成APP支付的对接。现在要玩点新花样,找人代付订
单。用户在APP中下单后选择找人代付,将分享H5页面到微信中的好
友。好友在微信中打开H5页面,H5页面展示订单信息,并完成代付。
业务分析
代付功能在某些2C的应用中会用到,当然涉及支付的应用都可以有代付
功能。本文分析如何实现微信代付功能的技术设计。
根据微信支付的官方文档,微信支付对接支持JSAPI支付、APP支付、H5
支付、Native支付、小程序支付等,实际业务中应当根据使用的API类型
完成支付和代付功能设计。需要注意:微信没有代付接口,所谓的代付功
能实际是想微信发起支付API的对接。
还需要注意,微信支付对接在开发之前需要根据使用场景做接入准备,具
体可以阅读微信支付官方文档。
统一下单与支付
微信支付前需要先向微信发起“统一下单”,如果统一下单请求正常微信会返回预支
付ID,预支付id是支付接口需要用到参数。
有个原则,如果使用JSAPI拉起支付,则需要用JSAPI先发起“统一下
单”,如果使用APP支付,则需要先请求APP支付的统一下单。
不同的支付对接不同混用“统一下单”。
在微信内完成代付
大多数代付,是通过分享完成的。注意分享是第一步,也是比较重要的一步。
为什么很多代付是基于微信支付实现的,因为社交软件的特点催生了好友代付的需
求场景。
微信内支付
另外出于对后安全性的考虑,分享出去的页面只能在微信中打开。于是可以选择的
代付支付方式有:1.微信JSAPI支付:商户已有H5商城网站,用户通过消息或扫描二维码在微信内打开网页时。2.小程序支付:商户已有微信小程序,用户通过好友分享或扫描二维码在微信内打开小程序时,可以调用微信支付完成下单购买的流程。
微信外支付可以阅读微信H5支付文档。
交互
APP与后端交互:在订单下单,分享环节的设计主要围绕APP与后端系统的交互。H5与后端的交互:在分享完成后的,订单数据信息、用户数据信息的加载与展示,拉起支付等操作。设计上应当考虑H5页面对用户数据、订单数据的敏感性与安全性,这里主要考虑接口的匿名访问权限设计与跨域问题。用户分享H5页面出去后,实际需要在访问H5页面时通过Get请求携带一些不要的参数,为安全考虑参数应当在生成分享链接前加密处理。
安全性设计
H5页面被浏览器渲染后,向后端请求用户数据和订单数据,后端必须进行安全性的校验。首先用户信息和订单数据信息敏感,但是根据代付的分享业务设计,还要具有可匿名访问和跨域的设计。1.跨域问题:将H5页面的访问地址固化,可在后端对H5页面中的匿名请求进行校验,注意Referer校验并不可靠,因为它可以伪造。2.请求签名:应当设计签名算法和逻辑,防止伪造。3.订单的“统一下单”一定要在后端实现4.参数先加密,不要存在明文参数,不要直接使用base64将明文参数进行编码,它不是加密算法。5.多次分享后的支付问题,同一笔订单支付一定要做后端的幂等处理。6.APP订单id与微信“统一下单”的预支付id一定要做数据库层面的逻辑关系绑定。
Java 后端与H5代码实现
- 开发H5分享代付页面
由于分享功能是APP内实现的,H5页面实际是独立部署在后端服务器上的,对H5的访问实际是通过分享出去的访问链接地址在微信内部的浏览器打开的。
<html><head><style type="text/css"></style></head><script type="text/javascript">//假设H5代付页面访问地址: http://h5.html?payId=xxxxxx加密串xx&sign=xx签名串xx//微信重定向后地址: http://h5.html?payId=xxxxxx加密串xx&sign=xx签名串xx&CODE=XX微信返回XX//微信静默授权地址://https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect //1.如果用户第一次打开分享链接,没有CODE参数会向微信请求授权,微信会重定向回来,并携带微信给的CODE参数var code =getUrlParamByName('CODE');if(!code){window.location.href='https://open.weixin.qq.com/connect/oauth2/authorize?'+'appid=xxxxxAPPIDxxxxxxxx'+'&redirect_uri= ' //此H5页面访问地址+ "http://h5.html?payId=xxxxxx加密串xx&sign=xx签名串xx"//静默授权+'&response_type=code&scope=SCOPE&state=STATE#wechat_redirect';}//获取请求地址中的参数function getUrlParamByName(name){var query = window.location.search.substring(1);var vars = query.split("&");for (var i=0;i<vars.length;i++) {var pair = vars[i].split("=");if(pair[0] == name){return pair[1];}}return '';}//因为微信支付需要prepayId 预支付ID//所以需要后台先请求微信统一下单并返回预支付ID给前端function prepay(){$.ajax({url:'/后端统一下单接口',data:{//统一下单需要用户openId//后端根据code向微信换取openIdcode:code //后台用code获取用户openId},success:function(res){//后台返回的预支付IDvar prepayId = res.prepayId;//拉起微信支付weixinPay(prepayId);}});}//拉起微信支付function weixinPay(prepayId){//微信 JSAPI 、 H5 拉起支付代码,具体可参见微信开放平台示例:
WeixinJSBridge.invoke('getBrandWCPayRequest', {"appId": "wx2421b1c4370ec43b", //公众号ID,由商户传入 "timeStamp": "1395712654", //时间戳,自1970年以来的秒数 "nonceStr": "e61463f8efa94090b1f366cccfbbb444", //随机串 "package": "prepay_id=up_wx21201855730335ac86f8c43d1889123400","signType": "RSA", //微信签名方式: "paySign": "oR9d8PuhnIc+YZ8cBHFCwfgpaK9gd7vaRvkYD7rthRAZ\/X+QBhcCYL21N7cHCTUxbQ+EAt6Uy+lwSN22f5YZvI45MLko8Pfso0jm46v5hqcVwrk6uddkGuT+Cdvu4WBqDzaDjnNa5UK3GfE1Wfl2gHxIIY5lLdUgWFts17D4WuolLLkiFZV+JSHMvH7eaLdT9N5GBovBwu5yYKUR7skR8Fu+LozcSqQixnlEZUfyE55feLOQTUYzLmR9pNtPbPsu6WVhbNHMS3Ss2+AehHvz+n64GDmXxbX++IOBvm2olHu3PsOUGRwhudhVf7UcGcunXt8cqNjKNqZLhLw4jq\/xDg==" //微信签名 },function(res) {if (res.err_msg == "get_brand_wcpay_request:ok") {// 使用以上方式判断前端返回,微信团队郑重提示://res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。}});}</script><body><div><span>好友 {{order.user.name}} 邀您代付 {{order.payAmount}} 元</span></div><div ><img src="{order.usr.photo}" alt="用户头像" /></div><div><ul><li v-for="item in order.orderItems"><img src="{item.photo}" alt="订单商品图片" /><span>{{item.productName}}</span><span>{{item.price}}</span><span>{{item.buyCount}}</span></li></ul></div><button id="payNow" click="prepay()">立即支付</button></body>
</html>
- 开发接口返回H5分享链接
如果没有特殊要求,H5页面开发万后可以部署在服务器上作为静态资源对外提供访问,就是指任意用户都可以直接访问,知道页面访问地址都可以访问页面。
如果安全一点的做法,就是将页面部署在受保护的服务中,要访问页面必须通过后端的校验之后才被返回,而无法直接通过地址直接发起对页面的请求。
这里使用静态资源部署H5页面,这也是为什么可以在H5代码中向微信静默获取用户授权的请求参数 redirect_uri 写成H5页面访问地址的原因。
如果H5是后台接口返回的话,redirect_uri 也要写成后台接口的地址。
/**
* APP获取H5代付,分享地址
* 此请求由APP内调用,非匿名接口
*/
@PostMapping
public Object appGetH5url(String orderId){//加密请求String payId = encrypt(PASSWORD,数据);//签名请求String url="http://h5.html?payId=xxxxxx加密串xx";String sign = MD5(url);return url+"&sign="+sign;
}
- 开发返回订单信息接口用于H5的订单与支付信息展示
H5 匿名接口,无需登录,要做好安全措施防止恶意请求和数据泄漏
/**
* H5请求支付订单信息
* @param data 待加密字符串
* @return 加密后内容
*/
@PostMapping
public Object queryOrder(String payId,String sign){//1.验证签名....if(checkSign(sign)==false){return "404.html";}//2.解密payIdMap<String,Object> param = decrypt(PASSWORD,payId);if(null==param){return "404.html";}String orderId= param.get("orderId");//查询订单数据后返回Order order = queryOrder(orderId);//订单状态,支付状态等 校验if(checkOrder(order)==false){return "404.html";}return order;
}
- H5页面“立即支付”前,调用后端“统一下单”获取prePayId
/**
* H5立即支付,后台微信统一下单
*/
@PostMapping
public Object queryOrder(String payId,String sign,String code){//1.验证签名....if(checkSign(sign)==false){return "404.html";}//2.解密payIdMap<String,Object> param = decrypt(PASSWORD,payId);if(null==param){return "404.html";}//3.向微信换取openIdString openId = 请求微信API(code);String orderId= param.get("orderId");//查询订单数据后返回Order order = queryOrder(orderId);//订单状态,支付状态等 校验if(checkOrder(order)==false){return "404.html";}//4.想微信统一下单String prepayId = 请求微信统一下单(order,openId,....);return prepayId;
}
- H5页面拉起微信支付
function weixinPay() {WeixinJSBridge.invoke('getBrandWCPayRequest', {"appId": "wx2421b1c4370ec43b", //公众号ID,由商户传入 "timeStamp": "1395712654", //时间戳,自1970年以来的秒数 "nonceStr": "e61463f8efa94090b1f366cccfbbb444", //随机串 "package": "prepay_id=up_wx21201855730335ac86f8c43d1889123400","signType": "RSA", //微信签名方式: "paySign": "oR9d8PuhnIc+YZ8cBHFCwfgpaK9gd7vaRvkYD7rthRAZ\/X+QBhcCYL21N7cHCTUxbQ+EAt6Uy+lwSN22f5YZvI45MLko8Pfso0jm46v5hqcVwrk6uddkGuT+Cdvu4WBqDzaDjnNa5UK3GfE1Wfl2gHxIIY5lLdUgWFts17D4WuolLLkiFZV+JSHMvH7eaLdT9N5GBovBwu5yYKUR7skR8Fu+LozcSqQixnlEZUfyE55feLOQTUYzLmR9pNtPbPsu6WVhbNHMS3Ss2+AehHvz+n64GDmXxbX++IOBvm2olHu3PsOUGRwhudhVf7UcGcunXt8cqNjKNqZLhLw4jq\/xDg==" //微信签名 },function(res) {if (res.err_msg == "get_brand_wcpay_request:ok") {// 使用以上方式判断前端返回,微信团队郑重提示://res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。}});
}
- 后端微信回调接口
/**
* 微信支付回调
*/
@PostMapping("/微信回调通知接口")
public void weichatNotify(){//判断支付结果,更新订单状态。。。
}