一、基本说明
微信小程序中对于用户的登录授权机制,是非常重要的。许多的业务都必须拿到具体的微信数据才能进行。我们在之前的微信小程序开发中,因为业务比较简单只有首页,并且在进入小程序的时候就必须进行授权,所以在操作的时候比较简单。随着业务的不断增加,业务场景的增多,之前的授权机制无法覆盖所有的业务场景。本次是在之前的授权机制的场景下,进行优化和改版。前端和后台进行配合,完善微信的授权机制,达到适应现有的业务。本文将讲解从老版本到现版本的微信授权机制的改版优化过程。
注: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中添加组件
这样优化后可以完善授权机制但是也有很大的弊端。因为微信没有顶层视图,所以导致了我们必须在每个需要授权的页面添加组件。如果再修改授权机制,每个页面都要进行维护,会产生很多的冗余代码,让项目变得难以维护。希望微信小程序之后可以考虑添加顶层视图,减少码农的痛苦。
本文将持续对授权机制的处理进行更新,谢谢大家的观看,有建议或者疑问欢迎给我留言。