1.背景
在小程序中,openid是一个用户对于一个小程序/公众号的标识,开发者可以通过这个标识识别出用户,就如同你的身份证一样。
2.什么是静默登录?
在普通的应用中,用户通过表单验证登录建立用户体系,这种常见的登录方式一般是通过登录页面表单进行登录,对用户来说是有感的。
在小程序中,由于是基于微信,可以通过微信官方提供的API能力,使我们能够无感知得获取用户身份标识(openid),快速建立小程序内的用户体系,对用户来说是无感知的,因此是由程序来完成这个自动的登陆过程。
2.1登录流程时序
下图取自微信官方

1// 小程序端调用wx.login(),获取code并且上传到服务器 2export async function doLogin() { 3 if (isLogin) return false 4 isLogin = true 5 removeCache('token') 6 const { code } = await wxp.login() 7 const data = await login({ code }) 8 setCache('token', data.data.token) 9 isLogin = false 10 return true 11} 12 13// 服务端拿到code,调用auth.code2Session接口换取openid 14const getOpenid = async function (appid, secret, code) { 15 const resData = await axios.get('https://api.weixin.qq.com/sns/jscode2session?appid=' + appid + '&secret=' + secret + '&js_code=' + code + '&grant_type=authorization_code'); 16 return resData.data; 17}
总结流程:
- 小程序端调用wx.login(),获取code并且上传到服务器
- 服务器根据code,并且调用微信auth.code2Session接口换取openid
- 后台服务器根据openid生成自定义token返回前端并且存储起来,后续业务逻辑用token来识别用户身份
3.如何维护自定义登录态
让我们来看下官方的处理方式:

1wx.checkSession({ 2 success () { 3 //session_key 未过期,并且在本生命周期一直有效 4 }, 5 fail () { 6 // session_key 已经失效,需要重新执行登录流程 7 wx.login() //重新登录 8 } 9})
由图中我们可以知道,真正决定登录态的是微信的checkSession接口。因此每次检查用户登录态是否有效就先调用一个checkSession接口,如果session_key失效,再发起登录流程。
4.静默登录整体流程
4.1app.onLaunch中发起登录
由于大部分的接口调用都需要token验证,因此在小程序启动的周期函数app.onLaunch中发起静默登录最为合适不过了。

4.2处理小程序不支持异步阻塞
由于小程序的启动流程中,页面级和组件级的生命周期函数都不支持异步阻塞;因此会造成一个情况,app.onLaunch中发起的wx.login还没有成功的时候,页面级的生命周期函数已经向服务器发起请求。由于我们的接口设计大部分都是需要验证的,此时登录还未成功,token也还没有正确返回,因此页面级的生命周期发起的数据获取接口肯定是会报错的(例如返回了401)
4.2.1粗糙的方案
采用回调函数的方式
1//app.js 2this.globalData.wxp.showLoading({ 3 title: '登录中...' 4 }); 5 await login(); 6 this.globalData.hasLogin = true; 7 if (this.checkLoginReadyCallback) { 8 this.checkLoginReadyCallback(); 9 } 10 this.globalData.wxp.hideLoading(); 11 12页面的生命周期中 13async onLoad() { 14 if (app.globalData.hasLogin) { 15 //如果已经登录了直接获取数据 16 this.getUserInfo(); 17 this.getEvent(); 18 } else { 19 //未登录定义下回调函数,等app.js登录成功之后进行调用 20 app.checkLoginReadyCallback = async () => { 21 this.getUserInfo(); 22 this.getEvent(); 23 }; 24 } 25 },
- 优点:简单粗暴
- 缺点:代码结构差;如果是多个页面为启动页,则需要多个页面都定义回调函数(假设使用了小程序onShare模式)
4.2.2优雅的方式
借助fly.js库,实现对请求进行上锁机制。流程:app.js中发起登录,同时页面中也会发起请求。在请求拦截器中判断请求的接口是否为白名单(不需要token验证的接口)接口和token是否存在;如果都为false,锁住当前请求进入请求队列,执行登录流程。等待登录流程成功之后解锁请求队列,继续发起页面级的请求任务。如下为请求拦截器中的代码:
1//拦截处理 2fly.interceptors.request.use(async (request) => { 3 //没有token且请求不是白名单的都锁住 4 if ( 5 !getCache('token') && 6 !whiteList.some((item) => request.url.startsWith(item)) 7 ) { 8 fly.lock() 9 //去登陆 成功之后再unlock 10 await doLogin() 11 fly.unlock() //解锁后,会继续发起请求队列中的任务 12 } 13 14 if (getCache('token') && !fly.config.headers['Authorization']) { 15 request.headers['Authorization'] = getCache('token') 16 } 17 request.headers['Content-Type'] = 'application/x-www-form-urlencoded' 18 19 return request 20})
当然,自定义登录态也会存在过期的情况,我们可以在响应拦截器中捕获出错进行处理:当检测到401token过期代码时,需要把请求队列后面的请求都锁死,防止多次出现401自定义登录态过期的情况,然后发起登录,登陆成功之后再进行解锁行为,触发后续的请求队列执行,并且重新执行本次由于token过期被服务器拒绝的接口,否则会造成请求失败的情况(由于静默登录是用户无感知的,突然出现身份验证信息过期会使用户感觉到特别地奇怪,因此需要重新执行本次请求操作而不是由用户再次点击或者其他的行为再发起):
1// 响应拦截 2fly.interceptors.response.use( 3 (response) => { 4 //只将请求结果的data字段返回 5 return response.data 6 }, 7 async (err) => { 8 if (err.status === 401) { 9 //401之后,把后面的请求都锁死 防止再次401 10 fly.lock() 11 removeCache('token') 12 //去登陆 成功之后再unlock 13 const isLoginSuccess = await doLogin() 14 if (isLoginSuccess) { 15 fly.unlock() 16 } 17 //新执行本次由于token过期被服务器拒绝的接口 18 return fly.request(err.request) 19 } 20 } 21)
由于请求有可能是并发的,为了防止登录被多次执行,因此对doLogin函数进行了小小的改造(尽管写得很不优雅,但是能力有限,大佬们赐教了):
1export async function doLogin() { 2 //如果正在登录中则不执行 3 if (isLogin) return false 4 isLogin = true 5 //修改状态为登录中,反正重复多次登录 6 removeCache('token') 7 const { code } = await wxp.login() 8 const data = await login({ code }) 9 setCache('token', data.data.token) 10 isLogin = false 11 return true 12}
4.3 整体流程图

5.写在最后
细节的读者即可发现,api请求中并未设置最大请求数量的(微信小程序最大支持五个api同时发起),这点是需要补充进来的。总体写下来作者觉得在实现方式上还有进步的空间,作者能力有限,也是一边学习一边探讨,如果哪里写得有问题,请给我留言指出,感谢!
