小程序扫码登录web端

1,116 阅读3分钟

前文《小程序云函数接口设计》所述,web管理后台界面上生成二维码,然后用小程序扫码进行登录,就可以在web端也带入用户的openid,从而使web与小程序使用同一套用户体系。

流程图如下:

扫码登录全流程图

具体步骤及伪代码:

一、web端生成二维码:

用户打开web后台界面,web前端与web后端建立socket连接后,web前端生成二维码,对应url形如 http://xxx.com/loginByMiniprogram?uuid=xxxx,uuid为了简化就使用socketId,当然为了安全起见,也可以自行用算法生成一个扫码专用的uuid。

1.1 web-api端

web端仅为管理员使用,访问量不大,所以为了简化,项目没有用redis等三方缓存,后台单进程运行即可。在app启动后,初始化一个全局socketClients变量,用来存储和判断socketId是否过期。

//app.js
class AppBootHook {
  constructor (app) {
    this.app = app
  }

  async didLoad () {
    this.app.cache = {}
    //初始化全局socketClients变量
    this.app.cache.socketClients = {}
  }
}

config配置里增加对egg-socket.io的支持

//config/config.default.js
  config.io = {
    namespace: {
      '/': {
        connectionMiddleware: [],
        packetMiddleware: []
      }
    }
  }

router.js里增加接收前端socket消息的路由

//router.js
module.exports = app => {
  const { router, controller, io } = app
  //...
  io.route('hello', controller.socket.hello)
  //....
}

在SocketController里增加对前端的hello消息的响应,发送uuid

//controller/socket.js
class SocketController extends Controller {
  async hello () {
    const { ctx, app } = this
    const id = ctx.socket.id
    const now = Date.now()
    app.cache.socketClients[id] = now
    ctx.socket.emit('uuid', id)
  }

1.2 web前端

在登录页面初始化socket,并接收uuid,生成url

//login.vue
<template>
<!--省略-->
<qrcode :value="url" :options="{ width: 200 }" v-if="!!url"></qrcode>
<!--省略-->
</template>

import io from 'socket.io-client'
import VueQrcode from '@chenfengyuan/vue-qrcode'
export default {
   //...
  data(){
    uuid: '',
  },
  computed: {
      url () {
      if (this.uuid) {
        return setQuery({ uuid: this.uuid }, `http://localhost:7001/loginByMiniprogram`)
      }
      return ''
    }
  },
  created () {
    const self = this
    const socket = io('http://localhost:7001/')
    socket.on('connect', () => {
      socket.emit('hello')
    })
    socket.on('uuid', (uuid) => {
      self.uuid = uuid
    })
    this.socket = socket
  },

二、小程序端扫码

2.1 小程序端

//demo.wxml
  <view>
    <button bindtap="scan">扫码登录管理台</button>
  </view>

小程序端先调用扫码方法scanCode,解析出url后,调用云函数方法loginQrCode,并将解析出的二维码url作为参数带入。

//demo.js
  scan: async function(){
    // 只允许从相机扫码
    try{
      let {result: url} = await app.globalData.wxp.scanCode({
        onlyFromCamera: true,
      })
      if(url){
        const {code, info} = await api.callCloud('loginQrCode',{url})
        if(code==='0000'){
          wx.showToast({
            title: info,
            icon: 'success',
            duration: 2000
          })
        }
      }
    }catch(e){}
  },

2.2 云函数端

云函数端将openId, appId等小程序云端才有的参数拼接上,get方式访问url,这里使用了axios库作为http的客户端,注意因为小程序仅支持commonjs引用,要加.default

//cloudfunctions/runFunc/index.js
const axios = require('axios').default

async function main(event, context) {
  let {fname, version, token, clientType, adminOpenId, ...opt} = event
  let result, role
  const { OPENID, APPID } = cloud.getWXContext()
  if(fname==='loginQrCode'){
  	//去云数据库里查询用户的名字,角色等信息
    result = await db.collection('user').where({openId: OPENID}).get()
    if(result.data && result.data.length){
      result = await axios.get(event.url, {params:{openId: OPENID, appId: APPID, exist: '1', userName: result.data[0].userName, role: result.data[0].role }})
    } else {
      result = await axios.get(event.url, {params:{openId: OPENID, appId: APPID, exist: '0' }})
    }
    if(result.data==='扫码成功'){
      return {code:'0000', info: result.data}
    } else{
      return {code:'0610', info: result.data}
    }
  }
  //...

三、api端根据get请求的参数,给web前端返回登录结果

3.1 web-api端处理get请求

在web-api端新增一个route

//router.js
router.get('/loginByMiniprogram', controller.adminusers.loginByMiniprogram)

在AdminUserscontroller里处理uuid的校验,对云端和web前端分别进行响应

//app/controller/adminusers.js
  async loginByMiniprogram () {
    const { ctx, app } = this
    const { uuid, openId, role, userName, appId, exist } = ctx.request.query
    if (appId !== app.config.appId) {
      ctx.body = '请在小程序里扫码'
      return
    }
    const nsp = app.io.of('/')
    const id = uuid
    if (id && nsp.sockets[id]) {
      const time = app.cache.socketClients[id]
      if (time) {
        if (Date.now() - time > 60000 * 5) {
          nsp.sockets[id].emit('token', { code: '0603', info: '二维码已过期' })
          ctx.body = '二维码已过期'
          delete app.cache.socketClients[id]
        } else {
          const { err, data } = await ctx.service.adminusers.loginByMiniprogram({ uuid, openId, role, userName, exist })
          if (err) {
            nsp.sockets[id].emit('token', { code: '0603', info: err.message || '用户无法登陆' })
          } else {
            nsp.sockets[id].emit('token', { code: '0000', data })
          }
          delete app.cache.socketClients[id]
          ctx.body = '扫码成功'
        }
      } else {
        ctx.body = '无效信息,无法登陆'
      }
    } else {
      ctx.body = '无效信息,无法登陆'
    }
  }

在AdminUsersService里校验uuid,合法的话生成token

//service/adminusers.js
  async loginByMiniprogram ({ uuid, exist, openId, role = '', userName = '' }) {
    const { app } = this
    if (!app.cache.socketClients[uuid]) {
      return { err: new Error('uuid不合法') }
    }
    if (exist !== '1') {
      return { err: new Error('用户不存在') }
    }
    if (role < 10) {
      return { err: new Error('没有管理员权限') }
    }
    const token = app.jwt.sign({ openId, role: parseInt(role) }, app.config.jwt.secret, { expiresIn: '1d' })
    return { err: null, data: { userName, token } }
  }

3.2 web端接收token并进行登录处理

//login.vue
//...
  created(){
     //...
    socket.on('token', ({ code, info, data }) => {
      if (code === '0000') {
        const { userName, token } = data
        //缓存token,改为已登录状态
      } else {
        //显示错误
      }
    })
  }