JS-SDK

1. 签名


看到网上的大部分问题都集中在签名部分,请大家一定请熟读微信JS-SDK说明文档附录5-常见错误及解决方法 部分。

注意

  • 在计算签名的过程中,如果url总是不对请 实验 首页的url或 window.location.href。 做到微信拿到真实路径与我们拿去生成签名的路径是一致的,千万记住这条

  • 前端需要用js获取当前页面除去'#'hash部分的链接(可用location.href.split('#')[0]获取,而且需要encodeURIComponent

  • vue每次刷新页面,都需要重新配置SDK,使用JS_SDK必须先注入配置信息

  • 关于html5-History模式在微信浏览器内的问题 #481

  • IOS:微信IOS版,微信安卓版,每次切换路由,SPA的url是不会变的,发起签名请求的url参数必须是当前页面的url就是最初进入页面时的url(entryUrl.js)

  • Android:微信安卓版,每次切换路由,SPA的url是会变的,发起签名请求的url参数必须是当前页面的url(不是最初进入页面时的)(entryUrl.js)

  • 登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。

  • 域名格式:如果你的项目域名是http://test.domain.com,那么JS接口安全域名为test.domain.com

  • timestamp: , // 生成签名的时间戳,精确到秒 秒 秒 秒

  • nonceStr: '', // 必填,生成签名的随机串

// entryUrl.js
 全局存储进入SPA的url(window.entryUrl),Android不变,依旧是获取当前页面的url,IOS就使用window.entryUrl
// 记录进入app的url,后面微信sdk
if (window.entryUrl === '') {
  window.entryUrl = location.href.split('#')[0]
}
// 进行签名的时候
url: isAndroid() ? location.href.split('#')[0] : window.entryUrl

支付流程图

payflow.png

2. 签名 (后台)

后台生成签名后返回给前台使用,很多微信api需要这个签名。

生成后可以验证一下签名是否正确 签名校验工具

//php
<?php
   namespace Vendor\wxpay;//命名空间
   /**
    * Class wxpay
    * @package Vendor\wxpay
    * @name 用于签名生产
    * @author weikai
    */
   class wxpay {
       private $appId;
       private $appSecret;

       public function __construct($appId, $appSecret) {
           $this->appId = $appId;
           $this->appSecret = $appSecret;
       }
       //获取签名
       public function getSignPackage() {
           $jsapiTicket = $this->getJsApiTicket();//获取JsApiTicket

           // vue管理路由 url参数建议前台传递
           $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
           // $url = "$protocol$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";//如果后台获取url
           $url = I('get.frontUrl');//获取前台传递的url
           $timestamp = time();//现在时间戳
           $nonceStr = $this->createNonceStr();//生成随机字符串

           // 这里参数的顺序要按照 key 值 ASCII 码升序排序
           $string = "jsapi_ticket=$jsapiTicket&noncestr=$nonceStr&timestamp=$timestamp&url=$url";

           $signature = sha1($string);//sha1加密排序后的参数生产签名
        //将所有参数赋值到数组
           $signPackage = array(
               "appId"     => $this->appId,
               "nonceStr"  => $nonceStr,
               "timestamp" => $timestamp,
               "url"       => $url,
               "signature" => $signature,
               "rawString" => $string
           );

           return $signPackage;
       }
     
    //生成随机字符串
       private function createNonceStr($length = 16) {
           $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
           $str = "";
           for ($i = 0; $i < $length; $i++) {
               $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
           }
           return $str;
       }
    //生成JsApiTicket
       private function getJsApiTicket() {
           // jsapi_ticket 全局存储与更新
           $data = json_decode(S('jsapi_ticket'));
         //如果缓存中的JsApiTicket 不在有效期内重新生成JsApiTicket 
           if ($data->expire_time < time()) {
               $accessToken = $this->getAccessToken();//获取AccessToken
               // 如果是企业号用以下 URL 获取 ticket
               // $url = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=$accessToken";
               $url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=$accessToken";
               $res = json_decode($this->httpGet($url));
               $ticket = $res->ticket;
             //更新有效期存入缓存
               if ($ticket) {
                   $data->expire_time = time() + 7000;
                   $data->jsapi_ticket = $ticket;
                   S('jsapi_ticket',json_encode($data));
               }
           } else {
             //否则缓存中的JsApiTicket 在有效期内就直接用
               $ticket = $data->jsapi_ticket;
           }

           return $ticket;
       }
    //获取全局AccessToken
       public function getAccessToken() {
           // access_token 全局存储与更新
           $data = json_decode(S('access_token'));
           if ($data->expire_time < time()) {
               // 如果是企业号用以下URL获取access_token
               // $url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=$this->appId&corpsecret=$this->appSecret";
               $url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=$this->appId&secret=$this->appSecret";
               $res = json_decode($this->httpGet($url));
               $access_token = $res->access_token;
               if ($access_token) {
                   $data->expire_time = time() + 7000;
                   $data->access_token = $access_token;
                   S('access_token',json_encode($data));
               }
           } else {
               $access_token = $data->access_token;
           }
           return $access_token;
       }

       //curl 请求
       private function httpGet($url) {
           $curl = curl_init();
           curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
           curl_setopt($curl, CURLOPT_TIMEOUT, 500);
           // 为保证第三方服务器与微信服务器之间数据传输的安全性,所有微信接口采用https方式调用,必须使用下面2行代码打开ssl安全校验。
           // 如果在部署过程中代码在此处验证失败,请到 http://curl.haxx.se/ca/cacert.pem 下载新的证书判别文件。
           curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);
           curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, true);
           curl_setopt($curl, CURLOPT_URL, $url);

           $res = curl_exec($curl);
           curl_close($curl);

           return $res;
       }


   }

3. 签名(前台)

  • [x] 微信配置
  • [x] 微信扫一扫
  • [x] 分享到朋友圈
  • [x] 朋友的设置
  • [x] 分享的基本配置
  • [x] 微信支付
/**
 * WECHAT.js
 * Created by  isam2016 
 */

import wx from 'weixin-js-sdk';
import axios from 'axios';

var JsWeChatApis = [
  'checkJsApi',
   请补全列表
];

var isWeChatReady = false;// 检查微信wx.ready
 
export default class AlWeChat {
  constructor(object) {
    this.object = object;// vue 需要使用vue 解决回调
    this.wxConfig();// 初始微信配置
  }
  /**
   * 微信配置
   * @Author   Hybrid
   * @DateTime 2017-11-21
   */
  wxConfig() {
    let self = this;
    axios.get('/home/OrderConfirm/wxConfig', {
      params: {
        frontUrl: location.href.split('#')[0]// 前台吧url 传到后台 而且需要encodeURIComponent,
      }
    }).then(function(response) {
      var attachment = response.data.data;// 后台统一调配数据,返回前台
      // console.log(attachment);
      wx.config({
        debug: false,
        appId: attachment.appId,
        timestamp: attachment.timestamp, // 支付签名时间戳小写s 时间戳(timestamp)值要记住精确到秒,不是毫秒。
        nonceStr: attachment.nonceStr,//支付签名随机串,不长于 32 位,大写s
        signature: attachment.signature,
        url: attachment.url,
        jsApiList: JsWeChatApis
      });
      wx.ready(function () {
        isWeChatReady = true;
        self.object && self.wxQDetailShare()//分享到朋友圈+朋友的设置
      });
      wx.error(function (res) {
        //console.log(JSON.stringify(res));
      });

    }).catch(function (error) {
      // console.log(error)
    });
  }

  /**
   * 微信扫一扫
   * @Author   Hybrid
   * @DateTime 2017-11-21
   * @return   {[type]}   [description]
   */
  wxScanQRCode(fn) {
    wx.scanQRCode({
      needResult: 1, // 默认为0,扫描结果由微信处理,1则直接返回扫描结果,
      scanType: ["qrCode"], // 可以指定扫二维码还是一维码,默认二者都有
      success: function (res) {
        var result = res.resultStr; // 当needResult 为 1 时,扫码返回的结果
        fn(result);
      }
    });
  }

  /**
   * 分享到朋友圈+朋友的设置
   * 可以动态设置
   * @Author   Hybrid
   * @DateTime 2017-11-21
   * @param    {}   data [展示数据]
   * @param    {[type]}   eqid [description]
   * @return   {[type]}        [description]
   */
  wxQDetailShare() {
    var config = {
      title: 'XXX',
      desc: 'XXX',
      imgUrl: 'XXX',
      link: 'XXX',
    };
    var shareConfig = {
      message: config,
      timeLine: {
        title: config.title,
        desc: config.desc,
        imgUrl: config.imgUrl,
        link: config.link,
      },
    };
    this.wxShare(shareConfig);
  }

  /**
   * 分享的基本配置
   * @Author   Hybrid
   * @DateTime 2017-11-21
   * @param    {}   shareConfig [不同类型的分享有不同的配置]
   * @return   {[type]}               [description]
   */
  wxShare(shareConfig) {
    let self = this;
    if (isWeChatReady) {
      /**
       * 分享到朋友圈
       * @Author   Hybrid
       */
      wx.onMenuShareTimeline({
        title: shareConfig.timeLine.title, // 分享标题
        link: shareConfig.timeLine.link, // 分享链接
        imgUrl: shareConfig.timeLine.imgUrl, // 分享图标
        success: function () {
          self.object.closecovershow();// 回调
          // 用户确认分享后执行的回调函数
        },
        cancel: function () {
          self.object.closecovershow();
          // 用户取消分享后执行的回调函数
        }
      });

      /**
       * 分享给朋友
       * @Author   Hybrid
       */
      wx.onMenuShareAppMessage({
        title: shareConfig.message.title, // 分享标题
        desc: shareConfig.message.desc, // 分享描述
        link: shareConfig.message.link, // 分享链接
        imgUrl: shareConfig.message.imgUrl, // 分享图标  type: '', // 分享类型,music、video或link,不填默认为link
        type: '', // 分享类型,music、video或link,不填默认为link
        dataUrl: '', // 如果type是music或video,则要提供数据链接,默认为空
        success: function () {
          self.object.closecovershow();
          // 用户确认分享后执行的回调函数
        },
        cancel: function () {
          self.object.closecovershow();
          // 用户取消分享后执行的回调函数
        }
      });
    }
  }


  /**
   * 微信支付
   * @Author   Hybrid
   * @DateTime 2017-11-21
   * @param    {string}   router 单页面应用,由前台通知URL
   * @return   {[type]}          [description]
   */
  payWeChat(order_num) {
    let self = this;
    axios.get('/home/OrderConfirm/orderPay', {
      params: {
        type: 'weixin',
        frontUrl: location.href.split('#')[0],
        order_num// 订单号
      }
    }).then(function (response) {
      var attachment = response.data.data;// 后台返回参数
      localStorage.setItem(wechatCode, '');
      //alert(location.href)
      WeixinJSBridge.invoke('getBrandWCPayRequest', {
        "appId": attachment.appId,
        "timeStamp": attachment.timeStamp, // 大写S
        "nonceStr": attachment.nonceStr, // 大写S
        "package": attachment.package,
        "signType": 'MD5',
        "paySign": attachment.paySign,
      }, function (res) {
        // console.log(res);
        if (res.err_msg == "get_brand_wcpay_request:ok") {
          self.object.$router.push("/paysuccess");
        } else if (res.err_msg == "get_brand_wcpay_request:cancel") {
          self.object.$router.push("/ordercenter");
        } else {
          // localStorage.setItem(wechatCodeOld, '');
          localStorage.setItem(wechatCode, '');
          //alert("支付失败!" + JSON.stringify(res) + "当前路径" + location.href);
          // alert("支付失败!" + JSON.stringify(res));
          //   resolve(-1);
        }
      })

    }).catch(function (err) {
      console.log(JSON.stringify(err));
    })
  }
}

在前端调用

/**
 * 注入配置
 * this 是 vue
 * 注意: 分享到朋友圈或分享到朋友,每个页面都需要配置.所以最好在每个页面调用一下 注入配置
 */
var WeChat = new AlWeChat(this) 
WeChat.payWeChat(12345678) // 调用支付
WeChat.wxScanQRCode(fn) // 扫一扫

3.微信支付

  • 支付申请:微信支付 - 公众号支付 -

  • 微信支付文档

  • 请仔细阅读公众号支付开发步骤

  • 设置支付目录

  • 设置支付授权目录注意3点:

    • 所有使用公众号支付方式发起支付请求的链接地址,都必须在支付授权目录之下;

    • 最多设置5个支付授权目录,且域名必须通过ICP备案;

    • 头部要包含http或https,须细化到二级或三级目录,以左斜杠“/”结尾。

  • 设置支付授权目录的具体规则是这样的:

1

比如:调用支付的页面地址为 http://a.b.com/pay/weixin/c.html,`
那么:授权目录配置为 http://a.b.com/pay/weixin/`
2
     
比如:调用支付的页面地址为 http://a.b.com/pay/weixin,
那么:授权目录配置为 http://a.b.com/pay/
3

比如:调用支付的页面地址为 http://a.b.com/pay,
那么:授权目录配置为 http://a.b.com/
4

比如:调用支付的页面地址为  http://a.b.com/pay/weixin/c.html?name=mango,
那么:授权目录配置为  http://a.b.com/pay/weixin/
5(vue spa)

 比如:调用支付的页面地址为  http://a.b.com/#/pay/weixin/c.html?name=mango
 那么:授权目录配置为   http://a.b.com/
6(vue spa)

 比如:调用支付的页面地址为  http://a.b.com/#!/cart/index(通常我们会改变URL http://a.b.com/?#!/cart/index)
 那么:授权目录配置为   http://a.b.com/

我们在网页授权的时候改变url

 location.href = `http://m.example.com/?a=1#${location.href.split('#')[1]}`; // 增加a=1 防止支付错误 防止前台死循环

PHP

我们基于微信支付官方demo做了优化(微信公众号支付)

源码目录 :

wxpay\wxjsapi.php

使用方法:

1. 将源码文件放到Thinkphp框架如下目录内

   项目名/ThinkPHP/Library/Vendor/wxpay/wxjsapi.php

2. 微信支付方法

只需导入sdk文件,实例化sdk类 调用getParameters方法传入订单数据

 /*
  * php
  * @name 微信支付
  * @author weikai
  */
 public function orderPay(){
     $type = I('get.type');
     //微信jsapi 支付
     if($type=='weixin'){
         // 导入微信支付sdk
         Vendor('wxpay.wxjsapi');
         $wxpay=new \Weixinpay();//实例化sdk类
        //获取订单数据传入sdk  getParameters方法中
          $order_num = I('get.order_num');
          $orderData = M('order')->where('order_number='.$order_num)->find();
          if($orderData){
              $data=$wxpay->getParameters($orderData);
          }else{
              return $this->ajaxReturn(show(0,'无此订单'));
          }
        //最后返回支付签名信息及预支付id
         if($data){
             return $this->ajaxReturn(show(1,'签名信息',$data));
         }else{
             return $this->ajaxReturn(show(0,'签名信息失败'));
         }
     }
 }
3. 微信SDK getParameters方法配置 (路径:wxpay\wxjsapi.php内)
    /**
    * 获取jssdk需要用到的数据
    * @return array jssdk需要用到的数据
    */
    public function getParameters($orderData){
           $order=array(
               'body'=>"商品描述".$orderData['order_number'],// 商品描述(需要根据自己的业务修改)
               'total_fee'=>$orderData['total_price']*100,// 订单金额  以(分)为单位(需要根据自己的业务修改)
               'out_trade_no'=>$orderData['order_number'],// 订单号(需要根据自己的业务修改)
               'product_id'=>'10001',// 商品id(需要根据自己的业务修改)
               'trade_type'=>'JSAPI',// JSAPI公众号支付
               'openid'=>session('openid')// 获取到的openid
           );
           // 统一下单 获取prepay_id 具体参照源文件内
           $unified_order=$this->unifiedOrder($order);
           // 获取当前时间戳
           $time=time();
           // 组合jssdk需要用到的数据
           $config = $this->config;
           $data=array(
               'appId'=>$config['APPID'], //appid
               'timeStamp'=>strval($time), //时间戳
               'nonceStr'=>$this->getNonceStr(32),// 随机字符串
               'package'=>'prepay_id='.$unified_order['prepay_id'],// 预支付交易会话标识
               'signType'=>'MD5'//加密方式
           );
           // 生成签名
           $data['paySign']=$this->makeSign($data);
       
           return $data;
       
       }
     }

​ 成功返回信息如图:

[图片上传失败...(image-5aa7c9-1522549296208)]

返回参数说明:

appId:微信公众号标识

nonceStr:随机字符串

prepay_id:微信生成的预支付会话标识,用于后续接口调用中使用,该值有效期为2小时

paySign:支付签名

signType:签名类型

timeStamp:时间戳

4.前端处理支付
在 WECHAT.js 中paywechat方法

5.支付结果通知

    /**
     * 微信公众号支付回调验证
     * @return array 返回数组格式的notify数据
     */
    public function notify(){
         Vendor('wxpay.wxjsapi');//引入sdk
         $wxpay=new \Weixinpay();//实例化sdk类
        // 获取微信支付通知的xml数据
        $xml=file_get_contents('php://input', 'r');
        
        // 转成php数组
        $data=toArray($xml);
        // 保存原sign
        $data_sign=$data['sign'];
        // sign不参与签名
        unset($data['sign']);
        $sign=$wxpay->makeSign($data);
        // 判断签名是否正确  判断支付状态
        if ($sign===$data_sign && $data['return_code']=='SUCCESS' && $data['result_code']=='SUCCESS') {
            $result=$data;
            //将支付状态更新进订单表
           //其他业务代码
        }else{
            $result=false;
        }
        // 返回状态给微信服务器
        if ($result) {
            $str='<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';

        }else{
            $str='<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名失败]]></return_msg></xml>';
        }
        echo $str;
        return $result;
    }

toArray -XML转换数组的函数

 /**
 * php
 * 将xml转为array
 * @param  string $xml xml字符串
 * @return array       转换得到的数组
 */
function toArray($xml){   
  //禁止引用外部xml实体
  libxml_disable_entity_loader(true);
  $result= json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);        
  return $result;
}

6.前台轮询判断订单支付状态,成功给用户提示。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,513评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,312评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,124评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,529评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,937评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,913评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,084评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,816评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,593评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,788评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,267评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,601评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,265评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,158评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,953评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,066评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,852评论 2 277

推荐阅读更多精彩内容