小程序、H5登錄授權、分享、支付流程

微信登錄、分享、支付流程

[TOC]

前言

對於前端來說,微信的支付分享登錄是一定要掌握的,今天這篇文章,主要對這三方面的流程進行詳細的介紹。主要內容如下:

域名相關知識介紹

  1. 業務域名:在微信瀏覽器中點擊文本框,會彈出提示該網站不安全,請不要輸入密碼的提示,通過配置業務域名可以解決這個問題。
  2. JS接口安全域名:分享功能(js-sdk)時需要試用這個域名。
  3. 網頁授權域名:用於獲取用戶針對於公衆號的唯一標識openid。

微信小程序授權登錄流程

op=>operation: openid判斷是否登錄授權
op2=>operation: 根據wx.login獲取code
op3=>operation: 調用服務端根據code換取openid
op4=>operation: 通過用戶授權,獲取信息,存到數據庫
op->op2->op3->op4

如果你從來沒有閱讀過小程序登錄授權的文檔,建議你看一下下面的地址:

服務端官方文檔客戶端文檔

nodejs 小程序實現授權登錄

前端部分

  1. 根據本地是否有userId判斷是否登錄,如果沒有登錄,則獲取用戶的openid
onLoad() {
   if(!this.data.userId) {
       this.getSession()
   }
},
 getSession() {
   wx.login({
     success: (res) => {
       if (res.code) {
         app.get(Api.getSession, {
           code: res.code
         }).then(res => {
           store.setItem('openid', res.openid)
         })
       }
     }
   })
 }
  1. 點擊授權按鈕,發起登錄請求。
getUserInfo(e) {
    let userInfo = e.detail.userInfo;
    userInfo.openid = store.getItem('openid')
    app.get(Api.login, {
        userInfo
    }).then(res => {
        store.setItem('userId', res.data.userId)
        this.setData({
            userId: res.userId
        })
    })
}

服務端部分

config裏面,定義公用的appidappsecret

module.exports = {
    wx: {
        appId: 'wx0ef10432747d8f57',
        appsecret: 'cc47a6127687e999a1dffa756ff83c0e'
    },
    mp: {
        appId: 'wx0691f1dcf6e5e231',
        appSecret: 'c7ed875e338120f15f49476a6596eb4f'
    }
}

然後通過調用小程序官方文檔的接口,獲取到appid傳給客戶端

let express = require('express');
let router = express.Router();
let request = require('request');
let config = require('./config');
let uril = require('./../../util/index')
config = Object.assign({}, config.mp);

router.get('/getSession', (req, res) => {
    let code = req.query.code
    if (!code) {
        res.json(uril.handleFail('code不能爲空', 10001))
    }
    let sessionUrl = `https://api.weixin.qq.com/sns/jscode2session?appid=${config.appId}&secret=${config.appSecret}&js_code=${code}&grant_type=authorization_code`;
    request(sessionUrl, (err, response, body) => {
        let result = util.handleResponse(err, response, body)
        res.json(result)
    })
})

登錄接口的編寫

// 小程序授權登錄
router.get('/login',async function(req,res){
  let userInfo = JSON.parse(req.query.userInfo);
  if (!userInfo){
    // 如果接口沒有信息,則返回錯誤信息
    res.json(util.handleFail('用戶信息不能爲空',10002))
  }else{
    // 查詢當前用戶是否已經註冊
    let userRes = await dao.query({ openid: userInfo.openid},'users_mp');
    if (userRes.code == 0){
      // 如果已經註冊,直接把查出來的信息返回給客戶端
      if (userRes.data.length >0){
        res.json(util.handleSuc({
          userId: userRes.data[0]._id
        }))
      }else{
        // 如果這個用戶之前沒有註冊,則在數據庫插入用戶信息
        let insertData = await dao.insert(userInfo,'users_mp');
        if (insertData.code == 0){
          let result = await dao.query({ openid: userInfo.openid }, 'users_mp');
          res.json(util.handleSuc({
            userId: result.data[0]._id
          }))
        }else{
          res.json(insertData);
        }
      }
    }else{
      res.json(userRes);
    }
  }
})

上述代碼的handleFailhandleResponse是封裝的對數據的統一處理,如果有興趣,參見github地址。這裏不展示代碼。

需要注意的是,這種實現方式,獲取openid的行爲放在後端實現了。如果放在前端實現也可以,但是會相對比較麻煩一點。此時,suerId就已經在數據庫存儲,並且在本地保存了,下次登錄的時候,如果有userId存在就不需要再次登錄了。

H5的登錄授權和分享流程

H5的登錄授權略有不同。如果用戶登錄授權頁面,發現該用戶沒有登錄授權,則需要跳轉到授權頁面。官方文檔給出的流程如下:

1 第一步:用戶同意授權,獲取code2 第二步:通過code換取網頁授權access_token3 第三步:刷新access_token(如果需要)4 第四步:拉取用戶信息(需scope爲 snsapi_userinfo)5 附:檢驗授權憑證(access_token)是否有效

在項目中代碼如下:(這裏代碼沒有實現刷新access_token和拉取用戶信息)

頁面加載的時候,判斷是否已經授權。

mounted(){
   this.checkUserAuth();
 },
methods:{
// 檢查用戶是否授權過
checkUserAuth(){
  let openId = this.$cookie.get('openId');
  if(!openId){
    // 如果沒有登錄授權,則跳轉到微信提供的跳轉頁面。
    window.location.href = API.wechatRedirect;
  }else{
  // 如果用戶已經授權,則調用獲取微信配置信息接口
    this.getWechatConfig();
  }
},

API.wechatRedirect:

wechatRedirect:'/api/wechat/redirect?url=http://m.51purse.com/#/index&scope=snsapi_userinfo',

「注意」

  • url地址需要encodeURIComponent編碼纔可以。
  • m.51purse.com需要與你在微信公衆號後臺配置的授權域名一致!

nodejs 對登錄授權回調接口的實現主要是拿到客戶端的請求參數,請求微信提供的接口

// 用戶授權重定向
router.get('/redirect',function (req,res) {
  let redirectUrl = req.query.url, scope = req.query.scope, callback = 'http://m.51purse.com/api/wechat/getOpenId';
  cache.put('redirectUrl', redirectUrl);
  // 獲取到客戶端帶過來的數據,請求微信接口
  let authorizeUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${config.appId}&redirect_uri=${callback}&response_type=code&scope=${scope}&state=STATE#wechat_redirect`;
  res.redirect(authorizeUrl);
})

當用戶點擊確認授權之後,會執行跳轉callbacl:http://m.51purse.com/api/wechat/getOpenId。而這個接口也是node端實現的,具體內容如下:

// 用code換取access_token的方法
exports.getAccessToken = function(code){
  let token_url = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${config.appId}&secret=${config.appSecret}&code=${code}&grant_type=authorization_code`;
  return new Promise((resolve, reject) => {
    request.get(token_url, function (err, response, body) {
      let result = util.handleResponse(err, response, body);
      resolve(result);
    })
  });
}


// 根據code獲取用戶的OpenId
router.get('/getOpenId',async function(req,res){
  let code = req.query.code;
  console.log("code:" code);
  if(!code){
    res.json(util.handleFail('當前未獲取到授權code碼'));
  }else{
    // 用code換取access_token
    let result = await common.getAccessToken(code);
    if(result.code == 0){
      // 換取access_token成功
      let data = result.data;
      let expire_time = 1000 * 60 * 60 * 2;
      // 往客戶端寫入cookie:openId
      res.cookie('openId', data.openid, { maxAge: expire_time });
      let openId = data.openid;
      let userRes = await dao.query({ 'openid': openId },'users');
      if (userRes.code == 0){
        if (userRes.data.length>0){
          // 從數據庫查找到用戶信息後,回調到客戶端的頁面
          let redirectUrl = cache.get('redirectUrl');
          res.redirect(redirectUrl);
        }else{
          let userData = await common.getUserInfo(data.access_token, openId);
          let insertData = await dao.insert(userData.data,'users');
          if (insertData.code == 0){
            // 從數據庫查找到用戶信息後,回調到客戶端的頁面
            let redirectUrl = cache.get('redirectUrl');
            res.redirect(redirectUrl);
          }else{
            // 返回錯誤信息
            res.json(insertData);
          }
        }
      }else{
        // 返回錯誤信息
        res.json(userRes);
      }
    }else{
      // 返回錯誤信息
      res.json(result);
    }
  }
})

「注意」:上面的代碼爲了簡單,刪除了一些不必要的代碼,如有興趣,訪問gitHub。

H5分享流程

同樣,如果你沒有閱讀過微信H5開發的官方文檔,建議你先閱讀。關於分享,你應該閱讀以下內容:

當再次回調到頁面的時候,從cookie已經拿到openId了。客戶端會繼續執行下面的代碼。獲取到服務端返回的配置信息,從而初始化分享的功能。

在這之前,你需要 npm install wx-jssdk

// 這個信息統一定義在api.js中,這裏爲了方便,放在前面,便於查看。
API.wechatConfig: /api/wechat/jssdk


// 獲取微信配置信息
getWechatConfig(){
  this.$http.get(API.wechatConfig '?url=' location.href.split('#')[0]).then(function(response){
    let res = response.data;
    if(res.code == 0){
      let data = res.data;
      wx.config({
        debug: true, // 開啓調試模式,調用的所有api的返回值會在客戶端alert出來,若要查看傳入的參數,可以在pc端打開,參數信息會通過log打出,僅在pc端時纔會打印。
        appId: data.appId, // 必填,公衆號的唯一標識
        timestamp: data.timestamp, // 必填,生成簽名的時間戳
        nonceStr: data.nonceStr, // 必填,生成簽名的隨機串
        signature: data.signature,// 必填,簽名
        jsApiList: data.jsApiList // 必填,需要使用的JS接口列表
      })
      wx.ready(()=>{
        util.initShareInfo(wx);
      })
    }
  })
}

util/index.js 裏面對分享的功能進行了封裝。

export default {
  //獲取瀏覽器地址欄參數值
  getUrlParam(name){
    let reg = new RegExp('(^|&)' name '=([^&]*)');
    let r = window.location.search.substr(1).match(reg);
    if(r!=null)return decodeURIComponent(r[2]);
  },
  initShareInfo(wx){
    let shareInfo = {
      title: 'xxxx', // 分享標題
      desc: 'xxxx', // 分享描述
      link: 'http://m.51purse.com/#/index', // 分享鏈接,該鏈接域名或路徑必須與當前頁面對應的公衆號JS安全域名一致
      imgUrl: '', // 分享圖標
    }
    wx.onMenuShareAppMessage(shareInfo);
    wx.onMenuShareTimeline(shareInfo);
    wx.onMenuShareQQ(shareInfo);
    wx.onMenuShareQZone(shareInfo);
    // 下面兩種方法爲新的方法,上面的方法將會被淘汰。
    wx.updateAppMessageShareData(shareInfo);
    wx.updateTimelineShareData(shareInfo);
  }
}

nodejs端對/wechat/jssdk接口的實現如下:

// common.getToken()方法 獲取基礎接口的Token

exports.getToken = function(){
  let token = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${config.appId}&secret=${config.appSecret}`;
  return new Promise((resolve, reject)=>{
    request.get(token, function (err, response, body) {
      let result = util.handleResponse(err, response, body);
      resolve(result);
    })
  })
}
----

router.get('/jssdk',async function(req,res){
  let url = req.query.url;
  let result = await common.getToken();
  if (result.code == 0){
    let token = result.data.access_token;
    let params = {
      // 生成隨機字符串
      noncestr:util.createNonceStr(),
      // 生成時間戳
      timestamp:util.createTimeStamp(),
      url
    }
      let str = util.raw(params);
      console.log('str:::'   JSON.stringify(params))
      let sign = createHash('sha1').update(str).digest('hex');
      res.json(util.handleSuc({
        appId: config.appId, // 必填,公衆號的唯一標識
        timestamp: params.timestamp, // 必填,生成簽名的時間戳
        nonceStr: params.noncestr, // 必填,生成簽名的隨機串
        signature: sign,// 必填,簽名
        jsApiList: [
          'updateAppMessageShareData',
          'updateTimelineShareData',
          'onMenuShareTimeline',
          'onMenuShareAppMessage',
          'onMenuShareQQ',
          'onMenuShareQZone',
          'chooseWXPay'
        ] // 必填,需要使用的JS接口列表
      }))
    }
  }else{
    res.json(result);
  }
})

以上代碼主要獲得基礎的token,然後用基礎token結合簽名、時間戳、隨機數等相關的參數,返回給客戶端相應的參數。

需要注意的是,基礎tokenaccessToken的區別。建議參考文章

到此,微信H5接入jssdk實現分享就已經完成了。

小程序支付

小程序支付前端流程

  • 獲取openId
  • 調起數字簽名

後端支付流程

  • 拼接常規參數
  • 生成簽名
  • 拼接xml數據
  • 調用下單接口
  • 獲取預支付Id:prepay_id
  • 生成支付sdk
  • 定義回調接口,接受微信支付消息

支付的主要邏輯在服務端

下面把服務端的流程通過代碼的方式表述出來。首先在util中封裝了一些支付需要的公共方法

/**
 * 公共函數定義
 */
let createHash = require('create-hash');
module.exports = {
  // 生成隨機數
  createNonceStr(){
    return Math.random().toString(36).substr(2,15);
  },
  // 生成時間戳
  createTimeStamp(){
    return parseInt(new Date().getTime() / 1000)   ''
  },
  // 生成簽名
  getSign(params, key){
    let string = this.raw(params)   '&key='   key;
    let sign = createHash('md5').update(string).digest('hex');
    return sign.toUpperCase();
  },
  // 生成系統的交易訂單號
  getTradeId(type='wx'){
    let date = new Date().getTime().toString();
    let text = '';
    let possible = '0123456789';
    for(let i=0;i<5;i  ){
      text  = possible.charAt(Math.floor(Math.random() * possible.length))
    }
    return (type == 'wx'?'ImoocWxJuZi':'ImoocMpJuZi')   date   text;
  },
  // Object 轉換成json並排序
  raw(args){
    let keys = Object.keys(args).sort();
    let obj = {};
    keys.forEach((key)=>{
      obj[key] = args[key];
    })
    // {a:1,b:2} =>  &a=1&b=2
    // 將對象轉換爲&分割的參數
    let val = '';
    for(let k in obj){
      val  = '&'   k   '='  obj[k];
    }
    return val.substr(1);
  }
}

下面是對支付的方法的封裝,其中調用了util中的函數。客戶端調用的就是下面的order方法。

/**
 * 微信小程序、H5通用支付封裝
 */
let config = require('./../pay/config')
let request = require('request')
let util = require('../../util/util')
let createHash = require('create-hash')
let xml = require('xml2js')
config = config.mch;
module.exports = {  
  order: function (appid,attach, body, openid, total_fee, notify_url, ip){
    return new Promise((resolve,reject)=>{
      let nonce_str = util.createNonceStr();
      let out_trade_no = util.getTradeId('mp');
      // 支付前需要先獲取支付簽名
      let sign = this.getPrePaySign(appid, attach, body, openid, total_fee, notify_url, ip, nonce_str, out_trade_no);
      // 通過參數和簽名組裝xml數據,用以調用統一下單接口
      let sendData = this.wxSendData(appid, attach, body, openid, total_fee, notify_url, ip, nonce_str, out_trade_no, sign);
      let self = this;
      let url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
      request({
        url,
        method: 'POST',
        body: sendData
      }, function (err, response, body) {
        if (!err && response.statusCode == 200) {
          xml.parseString(body.toString('utf-8'),(error,res)=>{
            if(!error){
              let data = res.xml;
              console.log('data:'   JSON.stringify(data));
              if (data.return_code[0] == 'SUCCESS' && data.result_code[0] == 'SUCCESS'){
                // 獲取預支付的ID
                let prepay_id = data.prepay_id || [];
                let payResult = self.getPayParams(appid, prepay_id[0]);
                resolve(payResult);
              }
            }
          })
        } else {
          resolve(util.handleFail(err));
        }
      })
    })
  },
  // 生成預支付的簽名
  getPrePaySign: function (appid, attach, body, openid, total_fee, notify_url, ip, nonce_str, out_trade_no) {
    let params = {
      appid,
      attach,
      body,
      mch_id: config.mch_id,
      nonce_str,
      notify_url,
      openid,
      out_trade_no,
      spbill_create_ip: ip,
      total_fee,
      trade_type: 'JSAPI'
    }
    let string = util.raw(params)   '&key='   config.key;
    let sign = createHash('md5').update(string).digest('hex');
    return sign.toUpperCase();
  },
  // 簽名成功後 ,根據參數拼接組裝XML格式的數據,調用下單接口
  wxSendData: function (appid, attach, body, openid, total_fee, notify_url, ip, nonce_str, out_trade_no,sign) {
    let data = '<xml>'   
      '<appid><![CDATA['   appid   ']]></appid>'   
      '<attach><![CDATA['   attach   ']]></attach>'   
      '<body><![CDATA['   body   ']]></body>'   
      '<mch_id><![CDATA['   config.mch_id   ']]></mch_id>'   
      '<nonce_str><![CDATA['   nonce_str   ']]></nonce_str>'   
      '<notify_url><![CDATA['   notify_url   ']]></notify_url>'   
      '<openid><![CDATA['   openid   ']]></openid>'   
      '<out_trade_no><![CDATA['   out_trade_no   ']]></out_trade_no>'   
      '<spbill_create_ip><![CDATA['   ip   ']]></spbill_create_ip>'   
      '<total_fee><![CDATA['   total_fee   ']]></total_fee>'   
      '<trade_type><![CDATA[JSAPI]]></trade_type>'   
      '<sign><![CDATA[' sign ']]></sign>'   
    '</xml>'
    return data;
  },
  getPayParams:function(appId,prepay_id){
    let params = {
      appId,
      timeStamp:util.createTimeStamp(),
      nonceStr:util.createNonceStr(),
      package: 'prepay_id='   prepay_id,
      signType:'MD5'
    }
    let paySign = util.getSign(params,config.key);
    params.paySign = paySign;
    return params;
  }
}

最後定義/pay/payWallet的支付接口,裏面調用公用的order方法。

// 小程序支付
router.get('/pay/payWallet',function(req,res){
  let openId = req.query.openId;//用戶的openid
  let appId = config.appId;//應用的ID
  let attach = "小程序支付課程體驗";//附加數據
  let body = "歡迎學習慕課首門支付專項課程";//支付主體內容
  let total_fee = req.query.money;//支付總金額
  let notify_url = "http://localhost:3000/api/mp/pay/callback"
  let ip = "123.57.2.144";
  wxpay.order(appId,attach,body,openId,total_fee,notify_url,ip).then((result)=>{
    res.json(util.handleSuc(result));
  }).catch((result)=>{
    res.json(util.handleFail(result.toString()))
  });
})

這裏的流程請參見官方描述。官方描述的非常清楚,這兒就不描述更多了,其實主要就是拼接一些參數,獲取簽名。然後根據簽名加上其他需要的參數(參見上述代碼)再憑藉xml的數據。然後再調用統一下單接口https://api.mch.weixin.qq.com/pay/unifiedorder。生成prepay_id之後,生成小程序端需要的一些參數,然後把這些參數返回個小程序客戶端,供小程序的客戶端調用微信小程序的支付功能。

小程序前端支付非常簡單,只是簡單的調用服務端提供的payWallet接口,傳入openIdmoney即可。然後獲取到相應的參數,調用微信提供的requestPayment拉起支付即可。

主要代碼邏輯如下:


pay() {
    app.get(Api.payWallet,{
      openId: Store.getItem('openId'),
      money: this.data.index
    }).then((res) => {
      // 支付
      wx.requestPayment({
        timeStamp: res.timeStamp,
        nonceStr: res.nonceStr,
        package: res.package,
        signType: res.signType,
        paySign: res.paySign,
        success: function (errmsg) {
          if (errmsg == 'requestPayment:ok') {
            wx.showToast({
              title: '支付成功',
              icon: 'success'
            });
          }
        },
        fail: function (res) {
          if (res.errMsg == 'requestPayment:fail cancel') {
            wx.showToast({
              title: '支付取消',
              icon: 'none'
            });
          } else {
            wx.showToast({
              title: res.errmsg,
              icon: 'none'
            });
          }
        }
      })
    });
  }

到這裏,小程序端的支付功能就已經實現了。

發佈了167 篇原創文章 · 獲贊 28 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章