突然发现好久没写博客了,这几年在学校待的真是越来越懒了.最近在公司实习,boss让做微信端的开发,以前一直在做PC浏览器端开发,所以说算是从零开始.期间遇到了无数的坑,在这里记录一下.一方面让自己再重新整理一下思路,另一方面也是帮助像我这样对支付原理不清楚,被官方文档给绕的出不来的哥们一个帮助.
##一.思路梳理
先贴一张流程图,看不懂?懒得看?没关系直接跳过…
1.1.用户在微信浏览器中打开商户的H5界面若尚未授权登录则先进入授权登录页面,用户授权之后可在回显地址中获取code(后续可利用该code获取用户信息)
code说明 : code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。1.2.拿到code之后发一个ajax请求把到服务器,服务器根据code和商户的appid,appsecret获取用户的access_token,openid等进而可以获取用户的基本信息,以json格式返回前台
1.3.ajax请求处理完成后可以根据实际需要把用户的基本信息写到用户微信缓存中
1.4.经过以上步骤,您已经获取到了用户的基本信息,接下来就是发起支付了.
1.5.当用户点击支付按钮的时候,发一个ajax请求到服务器,带上之前的openid.
服务器端拿到openid之后,调用微信的“统一下单接口”
1.6.调用成功后微信会返回一个组装好的xml,我们提取之中的消息(预支付id也在其中)以JSON形式返回给前台
1.7.前台将该JSON传参给微信内置JS的方法中,调其微信支付
1.8.支付成功后,微信会将本次支付相关信息返回给我们的服务器
1.9.服务器拿到微信的支付信息后,对信息进行解析,确定该支付信息的确是微信发过来的.然后返回给微信服务器,告诉它说我们收到信息了,不然微信服务器还会再次请求确认(如果不进行应答,则微信服务器会通过一定的策略定期重新发起通知)
1.10.参考第7步回到前端,微信支付的JSAPI调用后,若用户完成支付,则该JSAPI返回get_brand_wcpay_request:ok.前端开发可据此处理支付成功后的逻辑,通常为页面跳转.
以上为微信支付的整体思路,接下来对每一步进行具体分析. |
##二.具体步骤
###2.1授权登录
具体而言,网页授权流程分为四步:
1、引导用户进入授权页面同意授权,获取code
2、通过code换取网页授权access_token(与基础支持中的access_token不同)
3、如果需要,开发者可以刷新网页授权access_token,避免过期
4、通过网页授权access_token和openid获取用户基本信息(支持UnionID机制)
细节性的东西可以先参考官方文档,相信这一步问题最集中的地方应该就是code的获取了
补充一点:关于授权主要有两种snsapi_base和snsapi_userinfo.这里以snsapi_userinfo为例.
关于code的获取: 用户同意授权后,页面将跳转至 redirect_uri/?code=CODE&state=STATE。 其中 redirect_uri是授权后重定向的回调链接地址.我们的code就是在该地址下获取.这里封装了一个获取地址栏参数的方法
function getQueryString(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
var r = window.location.search.substr(1).match(reg);
if (r != null)
return unescape(r[2]);
return null;
}
然后code可以直接调用var code = getQueryString(“code”);
前端js代码
var center = {
init : function() {
},
enterWxAuthor : function() {
var wxUserInfo = localStorage.getItem("wxUserInfo");
alert("wxUserInfo="+wxUserInfo);
//localStorage.removeItem('wxUserInfo');
if (!wxUserInfo) {
var code = getQueryString("code");
if (code) {
getWxUserInfo();
center.init();
} else {
//没有微信用户信息,没有授权-->> 需要授权,跳转授权页面
//alert("登录...");
window.location.href = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid='
+ '此处填写公众号的appid'
+ '&redirect_uri='
+ window.location.href
+ '&response_type=code&scope=snsapi_userinfo#wechat_redirect';
alert("window.location.href="+window.location.href);
alert("code="+getQueryString("code"));
alert("redirect_uri="+getQueryString("redirect_uri"));
}
} else {
center.init();
}
}
}
服务器端java代码
/**
* 微信授权
*
* @param code使用一次后失效
* @return 用户基本信息
* @throws IOException
*/
@RequestMapping(value = "/authorization.do", method = RequestMethod.GET)
public void authorizationWeixin(@RequestParam String code, HttpServletRequest request, HttpServletResponse response)
throws IOException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
PrintWriter out = response.getWriter();
logger.info("RestFul of authorization parameters code:{}", code);
try {
String rs = WeChatUtil.getUserInfo(code);
out.write(rs);
logger.info("RestFul of authorization is successful.", rs);
} catch (Exception e) {
logger.error("RestFul of authorization is error.", e);
} finally {
out.close();
}
}
/**
* 根据code 获取授权的token 仅限授权时使用,与全局的access_token不同
* @param code
* @return
* @throws Exception
*/
public static String getUserInfo(String code)throws Exception{
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
HttpSession session = request.getSession();
String data = (String) session.getAttribute("WEIXIN_SQ_ACCESS_TOKEN");
String rs_access_token = null;
String rs_openid = null;
String url = ConstantUtil.WX_OAUTH_ACCESS_TOKEN_URL + "?appid="+ConstantUtil.APP_ID+"&secret="+ConstantUtil.APP_SECRET+"&code="+code+"&grant_type=authorization_code";
if (StringUtils.isEmpty(data)) {
//已过期,需要刷新
String hs = HttpUtils.sendGet(url, null);
JSONObject json = JSONObject.parseObject(hs);
String refresh_token = json.getString("refresh_token");
String refresh_url = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid="+ConstantUtil.APP_ID+"&grant_type=refresh_token&refresh_token="+refresh_token;
String r_hs = HttpUtils.sendGet(refresh_url, null);
JSONObject r_json = JSONObject.parseObject(r_hs);
String r_access_token = r_json.getString("access_token");
String r_expires_in = r_json.getString("expires_in"); //有限时间 一般为2小时
rs_openid = r_json.getString("openid");
rs_access_token = r_access_token;
session.setAttribute("WEIXIN_SQ_ACCESS_TOKEN", r_access_token);
session.setAttribute("openid", rs_openid);
session.setMaxInactiveInterval(Integer.parseInt(r_expires_in) - 3600);
logger.info("Set sq access_token to redis is successful.parameters time:{},realtime",Integer.parseInt(r_expires_in), Integer.parseInt(r_expires_in) - 3600);
}else{
//还没有过期
String hs = HttpUtils.sendGet(url, null);
JSONObject json = JSONObject.parseObject(hs);
rs_access_token = json.getString("access_token");
rs_openid = json.getString("openid");
logger.info("Get sq access_token from redis is successful.rs_access_token:{},rs_openid:{}",rs_access_token,rs_openid);
}
return getOauthUserInfo(rs_access_token,rs_openid);
}
public static String getOauthUserInfo(String access_token, String openid) {
String url = "https://api.weixin.qq.com/sns/userinfo?access_token="+ access_token +"&openid="+ openid +"&lang=zh_CN";
try {
String hs = HttpUtils.sendGet(url, null);
//保存用户信息
//具体信息参见https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
// saveWeixinUser(hs);
//System.out.println("用户信息如下:"+hs);
return hs;
} catch ( Exception e) {
logger.error("RestFul of authorization is error.",e);
}
return null;
}
###2.2发起支付
简单的说就是调用微信浏览器内置js(getBrandWCPayRequest)发起支付,参考官方文档中的代码如下
function onBridgeReady(){
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId":"wx2421b1c4370ec43b", //公众号名称,由商户传入
"timeStamp":"1395712654", //时间戳,自1970年以来的秒数
"nonceStr":"e61463f8efa94090b1f366cccfbbb444", //随机串
"package":"prepay_id=u802345jgfjsdfgsdg888",
"signType":"MD5", //微信签名方式:
"paySign":"70EA570631E4BB79628FBCA90534C63FF7FADD89" //微信签名
},
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();
}
你要做的就是提供如下参数
| 名称 | 变量名 | 示例值 | 描述 |
| :-------- | --------😐 :–: | |
| 公众号id | appId | wx8888888888888888 |商户注册具有支付权限的公众号成功后即可获得 |
| 时间戳 | timeStamp| 1414561699 | 当前的时间的时间戳 |
| 订单详情扩展字符串| package | prepay_id=123456789 | 统一下单接口返回的prepay_id参数值,提交格式如:prepay_id=*** |
| 随机字符串 | nonceStr | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 随机字符串,不长于32位 |
| 签名方式 | signType | MD5 |签名类型,默认为MD5,支持HMAC-SHA256和MD5。注意此处需与统一下单的签名类型一致 |
| 签名 | paySign | C380BEC2BFD727A4B6845133519F3AD6 |签名,详见签名生成算法 |
那么问题来了,如何获取这些参数呢?不要慌问题不大
用户在点击支付按钮的时候,先发送一个ajax请求(带上用户的openid)到服务器,让服务器来获取这些参数,最终以json的形式返回.
先上代码
function pay(){
var wxUserInfo = localStorage.getItem("wxUserInfo");
var objwxUserInfo = JSON.parse(wxUserInfo);
var openid = objwxUserInfo.openid;
$.post('<%=ctxPath%>/wechat/pay.do?', {openid:openid}, function(result) {
function onBridgeReady(){
WeixinJSBridge.invoke(
'getBrandWCPayRequest',
result
, function(res){
// alert(JSON.stringify(res));
if(res.err_msg == "get_brand_wcpay_request:ok" ) {
//doit 这里处理支付成功后的逻辑,通常为页面跳转
window.location.href="<%=ctxPath%>/wechat/details.do"
}
}
);
}
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();
}
}, 'json');
}
可以看到上面的result就是服务器返回的json格式的参数
那么服务器是怎么获取的呢…
这里可以直接借用官方提供的SDK,当然也可以自己根据官方提供的接口一步步获取封装.
这里使用的是官方sdk
@RequestMapping(value = "/pay.do", method = RequestMethod.POST)
@ResponseBody
public Map<String, String> pay(String openid, HttpServletRequest request, HttpServletResponse response) throws Exception {
WXPayConfigImpl config = new WXPayConfigImpl();
WXPay wxpay = new WXPay(config,SignType.MD5,false); //true表示使用沙箱
Map<String, String> data = new HashMap<String, String>();
data.put("body", "XXXXX");
data.put("out_trade_no", WeChatUtil.getOrderIdByTime());//保证订单号不重复即可
data.put("device_info", "WEB");
data.put("fee_type", "CNY");
data.put("total_fee", "1");
data.put("openid", openid);
data.put("spbill_create_ip", WeChatUtil.getRemoteHost());
data.put("notify_url", ConstantUtil.NOTIFY_URL);//回调地址
data.put("trade_type", "JSAPI"); // 此处指定为 公众号支付
Map<String, String> resp = null;
Map<String, String> result = null;
try {
resp = wxpay.unifiedOrder(data);
System.out.println("resp="+resp);
result = WeChatUtil.respToResult(resp);
System.out.println("result = "+result);
} catch (Exception e) {
logger.debug("支付失败"+e);
}
return result;
}
/**
* 获取客户端真实IP地址
* @param request
* @return
*/
public static String getRemoteHost(){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ip = request.getHeader("x-forwarded-for");
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
ip = request.getHeader("Proxy-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
ip = request.getHeader("WL-Proxy-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
ip = request.getRemoteAddr();
}
return ip.equals("0:0:0:0:0:0:0:1")?"127.0.0.1":ip;
}
/**
* 提取统一下单的返回结果resp,转换成调用微信支付JSAPI所需要的参数
* @param resp
* @return
*/
public static Map<String, String> respToResult(Map<String, String> resp) {
Map<String, String> map = new HashMap<String,String>();
String packagepam = "prepay_id="+resp.get("prepay_id")+"";
map.put("appId", resp.get("appid"));
map.put("timeStamp", System.currentTimeMillis()+"");
map.put("nonceStr",WXPayUtil.generateNonceStr());
map.put("package", packagepam);
map.put("signType", "MD5");
String paySign = null;
try {
paySign = WXPayUtil.generateSignature(map, ConstantUtil.PARTNER_key);
} catch (Exception e) {
logger.debug("获取paySign失败"+e);
}
map.put("paySign", paySign);
return map;
}
至此服务器端数据准备完成并成功发给前端供JSAPI调用
用户支付完成之后,会调用我们之前设置的回调地址(即notify_url)
/**
* 解析微信发来的信息,通过重新签名的方式验证信息的正确性,确认信息是否是微信所发
* return_code和result_code都是SUCCESS的话,处理商户自己的业务逻辑
* 应答微信,告诉它说我们收到信息了,不用再发了(如果不进行应答,则微信服务器会通过一定的策略定期重新发起通知
* @return
*/
@RequestMapping("/afterPaySuccess.do")
public String afterPaySuccess() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.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"))) {
logger.debug(map.get("return_msg"));
return null;
}
// 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 null;
}
至此,整个支付流程就完成了 |
1.在最开始要先设置支付目录和授权域名
2.沙箱支付就是个坑,如果要用的话请做好心理准备
3.微信支付jsapi缺少参数 total_fee :可能原因是订单号之前用过了
4.记得坑还蛮多的,当时忘了记录,一下子想不起来了以后补充吧