微信小程序 微信授权前后端解决方案 (更新至5.10授权机制)

一、基本说明

微信小程序中对于用户的登录授权机制,是非常重要的。许多的业务都必须拿到具体的微信数据才能进行。我们在之前的微信小程序开发中,因为业务比较简单只有首页,并且在进入小程序的时候就必须进行授权,所以在操作的时候比较简单。随着业务的不断增加,业务场景的增多,之前的授权机制无法覆盖所有的业务场景。本次是在之前的授权机制的场景下,进行优化和改版。前端和后台进行配合,完善微信的授权机制,达到适应现有的业务。本文将讲解从老版本到现版本的微信授权机制的改版优化过程。
注:5月10日的授权方案可在第四部分直接查看

二、代码说明

(1) 1.0版本微信授权

本文对具体的小程序端和后台之间的微信授权不做具体的说明,详情请看文档。对于小程序我们封装了两个主要的工具模块类。用户模块user.js,请求模块request.js。
user.js主要处理用户的基本信息模块,包括授权管理和登录,用户其他数据的绑定等。
var user = {
  config:{},
  userinfo:null,
  // 初始化user模块,校验ukey
  init(data){
    this.config = data;
    var self = this;
    wx.checkSession({
      success:function(res){
        //存在登录状态
        self.checkUkey();
      },
      fail:function(){
        //过期-重新登录
        self.login();
      }
    });
  },
  // 发起微信登录接口 获取登录凭证code
  login(){
    var self = this;
    wx.login({
      success:function(res){
        self.config.code = res.code;
        wx.setStorageSync(self.config.storage.code,res.code);
        self.getWXUserInfo(true);
      },
      fail:function(res){
        console.log("user login fail")
      }
    })
  },
  // 获取用户信息
  getWXUserInfo(isthrough){
    var self = this;
    wx.getUserInfo({
      success:function(res){
        self.userinfo = res.userInfo;
        wx.setStorageSync(self.config.storage.wxUser, res.userInfo);
        if (isthrough) {
          self.getUserUkey(res.rawData);
        }
      },
      fail:function(res){
        wx.openSetting({
          success:function(res){
            if(res.authSetting["scope.userInfo"]){
              wx.getUserInfo({
                success: res => {
                  // 可以将 res 发送给后台解码出 unionId
                  self.userinfo = res.userInfo;
                  wx.setStorageSync(self.config.storage.wxUser, res.userInfo);
                  if (isthrough) {
                    self.getUserUkey(res.rawData);
                  }
                }
              });
            }
          }
        })
      }
    })
  },
  // 获取Ukey
  getUserUkey(rawData){
    var self = this;
    var data = {
      code: wx.getStorageSync(self.config.storage.code),
      rawData: rawData
    };
    wx.request({
      url:self.config.requrl+"/Wdservice/api/wxLogin",
      data: data,
      method:"GET",
      success:function(res){
        res = res.data;
        console.log(res);
        if(res.code==1){
          // 缓存Ukey          
           wx.setStorageSync(self.config.storage.ukey,res.result.ukey);

         // 缓存Ukey存储时间
          wx.setStorageSync(self.config.storage.ukeytime,new Date().getTime());
       }
      }
    })
  },
  // 检验ukey是否过期
  checkUkey(){
    var ukey = wx.getStorageSync(this.config.storage.ukey);
    var ukeyTime = wx.getStorageSync(this.config.storage.ukeytime);
    var tmpTime = new Date().getTime();
    var token = wx.getStorageSync(this.config.storage.ukey_licaiyi);
    if (tmpTime - ukeyTime >= 1000 * 60 * 60 * 12 || !token){
      ukey = "";
      this.login();
    } else {
      this.getWXUserInfo(false);
    }
  }
}

module.exports = user;

request.js主要对请求的再封装,统一加入ukey。可对接口统一管理,增加扩展性。
var app = getApp();

// 打包ukey和参数
function ukeyData (data) {

  var copyData = JSON.parse(JSON.stringify(data));
  var ukey = { "ukey": wx.getStorageSync(app.globalData.config.storage.ukey) };
  var okUkeyData = Object.assign(copyData, ukey);
  return okUkeyData;
}

function post(data){

  req({
    url:data.url,
    method:"POST",
    data:ukeyData(data.data),
    succ:data.succ,
    fail:data.fail,
    complete:data.complete
  })
}

function get(data){
  req({
    url:data.url,
    method:"GET",
    data: ukeyData(data.data),
    succ:data.succ,
    fail:data.fail,
    complete:data.complete
  })
}

function req(data){
  wx.request({
    url: data.url,
    data: ukeyData(data.data),
    method: data.method, // OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT
    header: {
      'content-type':'application/x-www-form-urlencoded',
    }, // 设置请求的 header
    success: function(res){
      // success
      res = res.data;
      if(typeof data.succ === "function"){
        data.succ(res);
      }
    },
    fail:function(res){
      if(typeof data.fail === "function"){
        data.fail(res);
      }
    },
    complete:function(res){
      if(typeof data.complete === "function"){
        data.complete(res);
      }
    }
  })
}

const request = {
  post:post,
  get:get
}

module.exports = request;

具体使用:在app.js中引用user.js初始化该组件即可。每次程序启动的时候会自动校验ukey的有效性,如果没有登录或者ukey过期会重新登录。
var user = require("/tools/user");
onLaunch: function () {
    // 检测用户
    user.init(config);
}

(2) 2.0版本微信授权

随着业务的增加,小程序添加了扫码支付的功能。该功能在1.0版本的情况下会出现很重大的bug。虽然在app.js中初始化了user.js组件并得到ukey等信息。但是跳转到扫码页面的时候,因为登录请求都是异步执行的,在app.js完成授权之前,界面上的接口已经调用了。由于当时可能还没有获取到ukey导致界面上的接口报错,而获取不到需要的数据,界面一片空白,又没有刷新机制,导致出现严重的授权bug。对于这个业务场景下1.0的微信授权已经无法解决我们的问题,所以我们对user.js进行优化,以适应该业务。
在user.js中添加登录完成的回调succ
var user = {
  config:{},
  userinfo:null,
  init(data){
    this.config = data;
    var self = this;
    wx.checkSession({
      success:function(res){
        //存在登录状态
        self.checkUkey();
      },
      fail:function(){
        //过期-重新登录
        self.login(null);
      }
    });
  },
  login(succ){
    var self = this;
    wx.login({
      success:function(res){
        self.config.code = res.code;
        wx.setStorageSync(self.config.storage.code,res.code);
        self.getWXUserInfo(true, succ);
      },
      fail:function(res){
        console.log("user login fail")
      }
    })
  },
  getWXUserInfo(isthrough, succ){
    var self = this;
    wx.getUserInfo({
      success:function(res){
        self.userinfo = res.userInfo;
        wx.setStorageSync(self.config.storage.wxUser, res.userInfo);
        if (isthrough) {
          self.getUserUkey(res.rawData, succ);
        }
      },
      fail:function(res){
        wx.openSetting({
          success:function(res){
            if(res.authSetting["scope.userInfo"]){
              wx.getUserInfo({
                success: res => {
                  // 可以将 res 发送给后台解码出 unionId
                  self.userinfo = res.userInfo;
                  wx.setStorageSync(self.config.storage.wxUser, res.userInfo);
                  if (isthrough) {
                    self.getUserUkey(res.rawData, succ);
                  }
                }
              });
            }
          }
        })
      }
    })
  },
  getUserUkey(rawData, succ){
    var self = this;
    var data = {
      code: wx.getStorageSync(self.config.storage.code),
      rawData: rawData
    };
    wx.request({
      url:self.config.requrl+"/Invest/api/wxLogin",
      data: data,
      method:"GET",
      success:function(res){
        res = res.data;
        if(res.code==1){
          wx.setStorageSync(self.config.storage.ukey,res.result.ukey);
          wx.setStorageSync(self.config.storage.ukeytime,new Date().getTime());
          self.getAuthenticationUserUkey(rawData, res.result.oid, succ);
        }
      }
    })
  },
  getAuthenticationUserUkey(rawData, openId, succ){
    var self = this;
    var data = {
      code: wx.getStorageSync(self.config.storage.code),
      rawData: JSON.parse(rawData),
      openId: openId,
      source: 2,
    };
    wx.request({
      url: self.config.couresurl + "/WechatApi/authentication",
      data: data,
      method: "POST",
      success: function (res) {
        res = res.data;
        if (res.resCode == 1) {
          wx.setStorageSync(self.config.storage.ukey_licaiyi, res.resObject);
          if (succ) {
            succ();
          }
        }
      }
    })
  },
  checkUkey(){
    var ukey = wx.getStorageSync(this.config.storage.ukey);
    var ukeyTime = wx.getStorageSync(this.config.storage.ukeytime);
    var tmpTime = new Date().getTime();
    var token = wx.getStorageSync(this.config.storage.ukey_licaiyi);
    if (tmpTime - ukeyTime >= 1000 * 60 * 60 * 12 || !token){
      ukey = "";
      this.login(null);
    } else {
      this.getWXUserInfo(false, null);
    }
  }
}

module.exports = user;

在扫码的界面上我们首先校验ukey,如果存在直接调用详细的接口,如果获取不到ukey,说明微信尚未授权,我们主动调用user模块的login方法,在登录的成功回调成功之后再执行接口,便可解决该问题。
if (wx.getStorageSync(app.globalData.config.storage.ukey)) {
    this.starToConfig();
 } else {
    app.globalData.wxUser.login(function(){
       self.starToConfig();
    });
 }
该解决方案具有很大的局限性,如果添加一个新的业务场景我们就需要多次判断,扩展性很差。但是对于只有这样一个特殊场景来说,还是适用的。

(3) 3.0版本微信授权

随着业务的增加,微信小程序进行了更大的改版,首页不再强制需要授权。我的页面则需要授权。在这样的背景下,我们对授权机制和请求机制进行联合,把授权的时机判断放在后台进行联合判断。这样就可以解决2.0遗漏下来的授权比较局限的问题。对于所有需要授权的地方后台如果检测到我们的接口用户没有授权,便返回统一的未授权的错误码,小程序端对该状态进行统一的操作,强制用户登录并在回调后执行后续的接口。
在request.js中, 我们对返回状态进行了细分,分为请求成功,请求错误,微信未授权等。
var app = getApp();

// 添加返回标识
var ResultCode = {
  ResultCodeSucc: 1,
  ResultCodeFail: -1,
  ResultCodeNoAccredit: -2
};

function ukeyData (data) {

  var okUkeyData = null;
  if (data) {
    var copyData = JSON.parse(JSON.stringify(data));
    var ukey = { "ukey": wx.getStorageSync(app.globalData.config.storage.ukey) };
    okUkeyData = Object.assign(copyData, ukey);
  } else {
    var ukey = { "ukey": wx.getStorageSync(app.globalData.config.storage.ukey) };
    okUkeyData = ukey;
  }
  return okUkeyData;
}

function post(data){
  req({
    url:data.url,
    method:"POST",
    data:ukeyData(data.data),
    succ:data.succ,
    error:data.error,
    no_accredit:data.no_accredit,
    fail:data.fail,
    complete:data.complete
  })
}

function get(data){
  req({
    url:data.url,
    method:"GET",
    data: ukeyData(data.data),
    succ:data.succ,
    error: data.error,
    no_accredit: data.no_accredit,
    fail:data.fail,
    complete:data.complete
  })
}

function req(data){
  wx.request({
    url: data.url,
    data: ukeyData(data.data),
    method: data.method, // OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT
    header: {
      'content-type':'application/x-www-form-urlencoded',
    }, // 设置请求的 header
    success: function(res){
      // success
      res = res.data;
     // 根据返回码 分发行为
     if (res.code == ResultCode.ResultCodeSucc) {
        // 请求成功
        if (typeof data.succ === "function") {
          data.succ(res);
        }
      } else if (res.code == ResultCode.ResultCodeNoAccredit) {
        if (typeof data.no_accredit === "function") {
          // 微信未授权
          app.globalData.wxUser.login(function () {
            data.no_accredit(res);
          });
        }
      } else {
        // 请求错误
        if (typeof data.error === "function") {
          data.error(res);
        }
      }
    },
    fail:function(res){
      // 请求失败
      if(typeof data.fail === "function"){
        data.fail(res);
      }
    },
    complete:function(res){
      if(typeof data.complete === "function"){
        data.complete(res);
      }
    }
  })
}

const request = {
  post:post,
  get:get
}

module.exports = request;

接口改造:
// 预约活动
  person_appointment_tap: function (e) {
    if (!this.data.isAppointment) {
      var self = this;
      request.post({
        url: app.globalData.config.requrl + "/Invest/Wx/subscribeMsg",
        data: {
          formid: e.detail.formId,
          activity_id: self.data.activity_id
        },
        header: {
          'content-type': 'application/x-www-form-urlencoded'
        },
        succ: function (res) {
          // 成功
          wx.showModal({
            title: '预约成功',
            content: "我们会在活动开始前给您发送消息提醒,请您及时关注",
            showCancel: false,
            confirmColor: "#ffcf00",
            success: function (res) {
            }
          })
          self.setData({ isAppointment: true });
        },
        error: function (res) {
          // 失败
          prompt.showModal(res.code == 2 ? "您已经预约过了!" : "预约失败,请稍后再试!", null);
          if (res.code == 2) {
            self.setData({ isAppointment: true });
          }
        },
        no_accredit: function (res) {
          // 在授权成功后再次调用该接口即可
          self.person_appointment_tap(e);
        }
      })
    }
  }
  
我们在app.js中删除用户初始化代码,在需要用户第一次授权的地方就进行初始化即可。在3.0中,我们将初始化授权代码放在我的单页的onLoad方法中,之后只要在后台需要授权权限的地方,检测到未授权,就可提示到前端,让前端进行用户登录授权了。

(4) 4.0版本微信授权(5月10日微信小程序授权调整)

5月10日微信小程序官方突然发布了新的微信小程序授权机制,用户首次授权必须使用按钮的方式进行显示授权。对于这次更新我们也在原有的基础上进行了改版,因为小程序的入口繁多而且微信没有继承的概念和像window一样的顶级浮层,就导致我们必须对授权机制进行统一管理。我们这次删除了user.js,将授权及整个登陆机制放在封装好的授权组件内进行统一管理。并在接口处处理登陆失效的情况,这样就能很好的解决这次微信新授权机制对我们的影响。
微信小程序获取用户信息接口优化调整文章 : 地址
(1)comp-auto组件
// index.js
var app = getApp();

Component({  
  properties: {
  },  
  data: {  
    show:false,
  },  
  methods: { 
    open(){
      this.setData({show:true});
    },
    close(){
      this.setData({show:false});
    },
      // 微信授权btn授权完成后会将用户信息返回,我们在这个方法中可以直接获取到用户数据
    userauth(){
      var self = this;
      wx.getUserInfo({
        success:function(res){
          wx.setStorageSync(app.globalData.config.storage.wxUser, res.userInfo);
          self.getUserUkey(res.rawData,true);
        },
        fail:function(res){
          console.log("userinfo fail",res);
        }
      })
    },
    checkUkey() {
      var ukey = wx.getStorageSync(app.globalData.config.storage.ukey);
      var ukeyTime = wx.getStorageSync(app.globalData.config.storage.ukeytime);
      var tmpTime = new Date().getTime();
      if (tmpTime - ukeyTime >= 1000 * 60 * 60 * 12) {
        wx.setStorageSync(app.globalData.config.storage.ukey, "");
        this.login();
      } else {
        this.getWXUserInfo();
      }
    },
    login(){
      var self = this;
      wx.login({
        success:function(res){
          wx.setStorageSync(app.globalData.config.storage.code,res.code);
          self.getWXUserInfo();
        },
        fail:function(res){
          console.log("user login fail")
        }
      })
    },
    getWXUserInfo() {
      var self = this;
      wx.getUserInfo({
        success: function (res) {
          wx.setStorageSync(app.globalData.config.storage.wxUser, res.userInfo);
          self.getUserUkey(res.rawData, false);
        },
        fail: function (res) {
          // 防止已授权的用户,主动在设置中关闭授权后,可以弹出授权组件
          self.open();
        }
      })
    },
    getUserUkey(rawData, close) {
      var data = {
        code: wx.getStorageSync(app.globalData.config.storage.code),
        rawData: rawData
      }
      var self = this;
      wx.request({
        url: app.globalData.config.requrl + "",
        data: data,
        method: "GET",
        success: function (res) {
          res = res.data;
          if (res.code == 1) {
            wx.setStorageSync(app.globalData.config.storage.ukey, res.result.ukey);
            wx.setStorageSync(app.globalData.config.storage.ukeytime, new Date().getTime());
            if (close) {
              self.close();
            }
          }
        },
        fail: function (res) {
          console.log(res);
        },
        complete: function () {

          // 授权完成并获取到用户数据后,给页面提供的回调
          self.triggerEvent("callback");
        }
      })
    },
    
    // 校验是否授权
    judgeAuth: function () {

      var self = this;
      this.close();
      wx.getSetting({
        success(res) {
          if (res.authSetting['scope.userInfo']) {
            // 已经授权关闭授权窗口
            // 直接调用已授权的方法
            self.close();
            self.triggerEvent("callback");
            return;
          } else {
            // 未授权去授权
            self.checkSession();
          }
        }
      })
    },

    checkSession: function () {

      var self = this;
      wx.checkSession({
        success: function (res) {
          //存在登录状态
          self.checkUkey();
        },
        fail: function () {
          //过期-重新登录
          self.login();
        }
      });
    }
  },
})
// index.wxml 用户可将授权button放在需要的地方
     
(2)request.js
修改之前接口未获取到用户信息的处理,删除自动登录代码将回调直接返回.request.js在3中给出了源码,这边就不再展示了。我们修改req方法。
function req(data){
  wx.request({
    url: data.url,
    data: ukeyData(data.data),
    method: data.method, // OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT
    header: {
      'content-type':'application/x-www-form-urlencoded',
    }, // 设置请求的 header
    success: function(res){
      // success
      res = res.data;
      if (res.code == ResultCode.ResultCodeSucc) {
        if (typeof data.succ === "function") {
          data.succ(res);
        }
      } else if (res.code == ResultCode.ResultCodeNoAccredit) {
        // 用户登录失效,将回调直接放回
        if (typeof data.no_accredit === "function") {
          data.no_accredit(res);
        }
      } else {
        if (typeof data.error === "function") {
          data.error(res);
        }
      }
    },
    fail:function(res){
      if(typeof data.fail === "function"){
        data.fail(res);
      }
    },
    complete:function(res){
      if(typeof data.complete === "function"){
        data.complete(res);
      }
    }
  })
}
(3)在page中使用comp-auto组件
1.在onShow方法中校验授权情况,未授权展示授权组件,让用户授权
  /**
   * 生命周期函数--监听页面显示
   */
  onShow: function () {
    this.selectComponent("#compauth").judgeAuth();
  },
2.书写授权组件回调,在此方法中书写只有授权才能调用的接口
  callback: function () {
    this.person_appointment_tap();
  }
3.修改需要授权才能使用的接口
 // 预约活动
  person_appointment_tap: function (e) {
    if (!this.data.isAppointment) {
      var self = this;
      request.post({
        url: app.globalData.config.requrl + "/Invest/Wx/subscribeMsg",
        data: {
          formid: e.detail.formId,
          activity_id: self.data.activity_id
        },
        succ: function (res) {
          // 成功
          wx.showModal({
            title: '预约成功',
            content: "我们会在活动开始前给您发送消息提醒,请您及时关注",
            showCancel: false,
            confirmColor: "#ffcf00",
            success: function (res) {
            }
          })
          self.setData({ isAppointment: true });
        },
        error: function (res) {
          // 失败
          prompt.showModal(res.code == 2 ? "您已经预约过了!" : "预约失败,请稍后再试!", null);
          if (res.code == 2) {
            self.setData({ isAppointment: true });
          }
        },
        no_accredit: function (res) {
          // 吊起授权组件登录方法,授权结束后会调用回调方法callback,再次请求该接口
        self.selectComponent("#compauth").login();
        }
      })
    }
  }
4.在index.json中添加组件
 {
  "usingComponents": {
   "compauth": "../../components/comp-auth/index"
  }
}
5.在index.wxml中添加组件

这样优化后可以完善授权机制但是也有很大的弊端。因为微信没有顶层视图,所以导致了我们必须在每个需要授权的页面添加组件。如果再修改授权机制,每个页面都要进行维护,会产生很多的冗余代码,让项目变得难以维护。希望微信小程序之后可以考虑添加顶层视图,减少码农的痛苦。

本文将持续对授权机制的处理进行更新,谢谢大家的观看,有建议或者疑问欢迎给我留言。

你可能感兴趣的:(微信小程序 微信授权前后端解决方案 (更新至5.10授权机制))