系列文章目录
使用eggjs+微信小程序开发一个时间管理小程序(一)——项目介绍
使用eggjs+微信小程序开发一个时间管理小程序(二)——项目搭建
使用eggjs+微信小程序开发一个时间管理小程序(三)——自动登录实现
前言
上篇文章留了一个尾巴,即如何实现小程序的自动登录。本篇文章将专题对该问题进行深入的解读和分享。
自动登录在小程序应用中非常普遍,用户点开小程序,当即完成登录/注册,而不是跳到登录页或其他操作,避免繁杂的操作导致用户退出。毕竟在现在这种卷死人的社会环境下,时间和效率就是一切。
一、小程序的用户体系
每个微信用户在某个小程序中,都有一个唯一的用户标识openid
。同一个微信用户,在不同小程序或公众号的openid
是不同的。
如果一个企业,有多种产品类型,比如公众号、小程序、app等,该如何打通不同产品的账号体系呢?
这就需要借助微信开放平台
提供的能力,企业在微信开放平台
创建账号,然后将公众号、小程序、app等绑定到该开放平台账号下。(必须是完成微信认证的企业号,要花钱,要有企业对公账户)
微信用户,在使用绑定在同一开放平台账号下的产品时,除了openid
之外,还有一个统一的unionid
。可以通过unionid
打通账号体系,将不同产品下的用户识别关联起来。
这块知识点是一个拓展,我的小程序是个人号,用户也只有openid,是最简单的情况。
二、小程序的登录流程
在自动登录的背景下,我们必须首先通过微信获得用户的openid,然后以openid为用户的唯一标识存进我们的数据库。当然,如果你们有登录注册页,则完全不用鸟这个。
1.流程介绍
- 在前端,使用wx.login方法,获取登录凭证code,该code有效时间5分钟,每次都不一样。
- 调用自己实现的登录接口,将code传过去。
- 在后端,调用微信提供的接口,https://api.weixin.qq.com/sns/jscode2session点我查看官方文档,该接口需要传入appid和appSecret,这俩是注册小程序的时候生成的唯一值,大家可以把它们存在后端代码中。该接口将返回用户的openid。
- 接下来,判断该openid是否在用户表中,如果没有就插入一条,如果有就直接登录。
- 在后端,登录实际上就是进行身份验证,登录时生成一个token返给前端,前端将其存起来,之后其他接口的请求中都在header中带上这个token。当请求走到后端时,后端验证该token是否有效,如果有效,则正常处理,如果失效,则返回401,让前端重新登录后再重新请求。这一切在用户那里,都是无感知的。
- 我采用的是JWT(JSON WEB TOKEN)这种身份验证方案,该方案在我另一篇文章中有详细解释传送门。
2.服务端代码
先将小程序的appid和appSecret设置为全局变量
// config.default.js
exports.miniAppId = "你的appid";
exports.miniAppSecret = "你的appSecret";
登录接口,每一步都有详细的注释
// service/user.js
async login() { // 登录
const { code } = this.ctx.request.body; // 接口传入的,通过wx.login获得的code
const { miniAppId, miniAppSecret } = this.config; // 从上面config中配置的全局变量
// 调用微信登录接口,获取session_key, unionid, openid
const res = await this.ctx.curl('https://api.weixin.qq.com/sns/jscode2session', {
data: {
appid: miniAppId,
secret: miniAppSecret,
js_code: code,
grant_type: 'authorization_code' // 这个值是写死的
},
dataType: 'json'
});
if (res && res.data.errcode) { // 如果微信接口调用失败,我们的接口也抛错出去
return {
status: 101,
message: res.data.errmsg,
result: res.data
}
} else { // 微信接口调用成功了
const { openid, session_key } = res.data;
// 开启事务
const result = await this.app.mysql.beginTransactionScope(async conn => {
try {
// 检查当前openid是否存在,不存在则创建一个用户
let userObj = {};
const checkAccount = await conn.select('user', {
where: {
open_id: openid
}
});
if (!checkAccount || !checkAccount.length) { // 不存在,创建用户
const now = Date.now();
const insertOpe = await conn.insert('user', [{
open_id: openid,
create_time: now,
update_time: now,
user_name: '微信用户', // 咱们不知道微信用户的昵称和头像,所以先默认填一个
url: 'https://img.speschool.com/default_avatar.png'
}]);
if (insertOpe.affectedRows !== 1) { // 如果插入操作由于各种原因失败了,则接口抛出错误
console.log('写入用户信息出错');
return {
message: '系统异常',
status: 501,
result: null
}
}
// 查出当前用户的信息,赋值给userObj
const queryUser = await conn.select('user', {
where: {
open_id: openid
}
});
if (queryUser && queryUser.length) {
userObj = queryUser[0];
}
} else { // 当前用户存在,直接将用户信息赋值给userObj
userObj = checkAccount[0];
}
// 登录成功,签发jwt 这里将一些用户的信息存放到jwt的对象中,生成了一个token。
// 之后调接口,验证token时,也可以直接获得这些值,这样接口就知道当前发起请求的用户是哪一个了
const token = JWT.sign({
userId: userObj.user_id,
userName: userObj.user_name,
openid: userObj.open_id,
url: userObj.url
}, this.config.jwt.secret, {
expiresIn: this.config.jwt.expiresIn
});
// 登录成功,返回用户信息和token
return {
message: '登录成功',
status: 200,
result: { ...userObj, token: token }
}
} catch (err) {
return {
message: '系统异常,请稍后再试',
status: 501,
result: null
};
}
});
return result;
}
}
3.前端代码
前端有如下调整:
- 调整上一篇中的myRequest方法,增加逻辑:
当接口请求401时,进行自动登录,登录成功后重新请求失败的接口调用。
- 增加一个自动登录的方法
来吧,代码展示~~~
// utils/request.js
export const myRequest = (config, resFn) => { // 增加了resFn这个入参,用于保留上一次失败请求的回调
const header = {
authorization: wx.getStorageSync('token') || ''
};
return new Promise((res, rej) => {
let url = config.url.replace(/^\//, '');
wx.request({
url: `${globalConfig.envSet.requestUrl}/${url}`,
data: config.data,
header: Object.assign({}, header, config.header || {}),
method: config.method || 'POST',
responseType: config.responseType,
success(reqRes) {
if (reqRes && reqRes.data) {
if (config.responseType == 'arraybuffer') {
res(reqRes.data);
} else {
if (reqRes.data.status == 200) {
res(reqRes.data);
if (resFn) { // 如果存在resFn,说明上一次操作登录失效了,要把它的promise回调也执行一次
resFn(reqRes.data);
}
} else if (reqRes.data.status == 401) {
// 登录一下之后,再重新发起请求
autoLogin(function () {
myRequest(config, res); // 这里的res是失败请求的res,也就是myRequest方法入参中的resFn
})
} else {
wx.showToast({
title: reqRes.data.message,
icon: 'none'
});
rej(reqRes.data);
}
}
}
},
fail(reqErr) {
rej(reqErr);
}
})
});
}
我们给
myRequest
增加了一个入参resFn
,用于记录上一次失败的请求的回调。
当上一次请求失败时,发起自动登录,登录完重新发起请求,并执行上次请求的回调,把成功请求的接口返回,返回给调用方。
页面P发起了一次R请求,实际上先发起R1报404,然后登录,再发起R2请求,然后把R2的返回结果返给P。
自动登录方法代码来了,就很简单
// utils/request.js
export const autoLogin = (cb) => {
// 发送 res.code 到后台换取 openId, sessionKey, unionId
wx.login({
success: result => {
// 登录
myRequest({
url: '/user/login',
data: {
code: result.code
}
}).then(res => {
if (res && res.status == 200) {
wx.setStorageSync('token', res.result.token);
wx.setStorageSync('userInfo', res.result);
if (cb && typeof cb === 'function') {
cb(res.result);
}
}
}, err => {
})
}
})
}
总结
以上,我们就完成了自动登录的实现
该方案的优势如下:
- 无需在app.js的onLaunch回调中登录
- 后续页面直接调接口,如果未登录,则自动登录。
- 后续页面调用接口时,无需考虑onLaunch的登录是否完成,无需处理onLaunch中登录接口和页面接口之间的时间差,减少思维量。