微信小程序扫码实现web自动登录

清明假期在家无聊,写了一个微信小程序扫码,web登录的demo

技术栈
前端:vue2+vue-socket.io+uuid,微信小程序原生+weapp.socket.io
后端: eggjs+redis+socket.io+qr-image
细分说明
web端流程
  • 打开登录页面
  • 请求获取二维码
    • 客户端先生成一个唯一值uuid, 携带uuid请求服务端,服务端并将uuid作为key,值为空,一个过期时间,存进redis中
    • 服务端利用uuid,作为qr-image的option,生成二维码(微信小程序扫码,获取的值就是uuid)
  • 浏览器可拿到二维码和uuid,vue-socket.io长连接监听uuid,是否绑定accessToken。如果有绑定值,调用获取用户信息的接口,web端成功登录;若在redis的过期时间内取不到,就说明二维码失效,重新刷新二维码。
微信小程序扫码流程
  • 注册/登录账号成功,生成accessToken。打开扫一扫,扫描web二维码
  • 扫描二维码后得到uuid
  • 确认按钮点击,socket将accessToken和uuid传递给服务端
  • 服务端验证accessToken和uuid,满足条件socket通知微信小程序最终确认授权
  • 微信小程序确认授权后,socket传递uuid和accessToken给web端。web端获取到accessToken,再走获取用户信息的接口。就达到web扫码登录的功能了。

大致的扫码流程就是如此。我呢做了一个基础版本。扫码,微信小程序redis绑定uuid和accessToken,再派发给web端,没有添加校验。

核心代码块
后端
获取二维码
const qr = require("qr-image");
   /**
   * @summary 获取二维码
   * @description 获取二维码
   * @router get /api/v1/user/generateQrCode
   * @request query integer size 大小 默认 1
   * @request query integer margin 二维码周围间距默认1
   * @request query string uuid 唯一标识
   * @response 200 baseResponse successed
   */
  async generateQrCode() {
    const { ctx, service } = this;
    const { size, margin, uuid } = ctx.query;
    try {
      // 大小默认5,二维码周围间距默认1
      const img = qr.image(uuid || "", {
        type: "png",
        size: size || 5,
        margin: margin || 1,
      });
      ctx.status = 200;
      const result = {
        img,
        type: "image/png",
      };
      await service.redis.set(uuid, "", 300); 
      ctx.body = img
      // ctx.body = ctx.helper.response({ data: result, msg: "生成成功" }); // 不能获取img对象赋值,直接src渲染
    } catch (e) {
      ctx.status = 414;
      ctx.set("Content-Type", "text/html");
      ctx.body = "

ERROR

"
; } } }
<img src="xxxx//api/v1/user/generateQrCode?uuid=xxx" />
egg 开启socket.io

官网链接

安装

$ npm i egg-socket.io --save

开启插件

// {app_root}/config/plugin.js
exports.io = {
  enable: true,
  package: 'egg-socket.io',
};

配置

// {app_root}/config/config.${env}.js
exports.io = {
  init: { }, // passed to engine.io
  namespace: {
    '/': { 
         // connectionMiddleware是在client保持连接的时候调用的中间件
         connectionMiddleware: [],
         // packetMiddleware是在server发送包给client之后调用的中间件
         packetMiddleware: [],
	},
    '/example': {
      connectionMiddleware: [],
      packetMiddleware: [],
    },
  },
};

socket.io规定文件夹

demo
├── app
│   ├── extend
│   │   └── helper.js
│   ├── io
│   │   ├── controller
│   │   │   └── default.js
│   │   └── middleware
│   │       ├── connection.js
│   │       └── packet.js
│   └── router.js
├── config
└── package.json

比如命名空间 " / "需要用到中间件 connection

// {app_root}/config/config.${env}.js
exports.io = {
  init: { }, // passed to engine.io
  namespace: {
    '/': { 
         // connectionMiddleware是在client保持连接的时候调用的中间件
         connectionMiddleware: ['connection'],
         // packetMiddleware是在server发送包给client之后调用的中间件
         packetMiddleware: [],
	},
  },
};

connection.js

const room = "default_room";

module.exports = () => {
  return async (ctx, next) => {
    // 权限校验通过
    ctx.socket.emit("res", "auth success");
    // 加入房间
    ctx.socket.join(room);
    ctx.socket.emit("dataState", {
      a: 3,
    }); // 测试

    ctx.socket.on("checkQrCodeAndSaveAccessToken", async (data) => { // 绑定 uuid : accessToken 键值
      if (data.qrCode) {
        const exist = await ctx.service.redis.isExistKey(data.qrCode);
        if (exist) {
          await ctx.service.redis.set(data.qrCode, data.accessToken, 60);
        } else {
          ctx.socket.emit("qrCodeStatusFromRedis", { status: 0 });
        }
      }
    });

    ctx.socket.on("sendQrCodeForAccessToken", async (data) => {  // 小程序确认,向web端派发accessToken
      if (data.qrCode) {
        const exist = await ctx.service.redis.isExistKey(data.qrCode);
        if (exist) {
          const accessToken = await ctx.service.redis.get(data.qrCode);
          ctx.socket.emit("sendAccessToken", { accessToken });
        } else {
          ctx.socket.emit("qrCodeStatusFromRedis", { status: 0 });
        }
      }
    });

    // 放行
    await next();
    console.log("断开连接");
  };
};
socket.io router-controller (扩展)

为了分离,方便维护代码

// {app_root}/app/router.js
module.exports = app => {
  const { router, controller, io } = app;
  router.get('/', controller.home.index);

  // socket.io  // 可以说成监听命名空间为"/" 上 exchange方法
  io.of('/').route('exchange', io.controller.nsp.exchange);
};

客户端只需要

socket.emit('exchange', {});

框架是以 Cluster 方式启动的,而 socket.io 协议实现需要 sticky 特性支持,否则在多进程模式下无法正常工作。

由于 socket.io 的设计,在多进程中服务器必须在 sticky 模式下工作,故需要给 startCluster 传递 sticky 参数。

修改 package.json 中 npm scripts 脚本:

{
  "scripts": {
    "dev": "egg-bin dev --sticky",
    "start": "egg-scripts start --sticky"
  }
}
前端
微信小程序
npm i weapp.socket.io

// 小程序端示例代码
const io = require('weapp.socket.io')

const socket = io('http://localhost:8005', {
  transports: ['websocket'],
  extraHeaders: {
    token: wx.getStorageSync('token'), // 在这里扩展,header增加了token,socket校验,为了严谨。此处token跟二维码token无关
  },
  reconnectionAttempts: 3, // 失败后重新连接次数,一直失败总共尝试四次
  reconnectionDelay: 2000, // 重新连接间隔时间毫秒
  forceNew: true,
})

socket.on('connect', function () {
  console.log('connected')
});

page({
  openscanCode() { // 扫一扫事件
    const that = this
    wx.scanCode({
      success(res) {
        if (res.errMsg === 'scanCode:ok') {
          const data = res.result
          that.setData({
            qrCode: data
          })

          socket.emit("checkQrCodeAndSaveAccessToken", {
            qrCode: data,
            accessToken: 'access_token'  // 做测试
          })
        }
      }
    })
  },
})
web
npm i vue-socket.io

注册
main.js

import VueSocketIO from 'vue-socket.io'

Vue.use(new VueSocketIO({
  debug: true,
  connection: 'http://127.0.0.1:8005/',
}))

页面

<template>
  <div id="app">
    <div>
      <img alt="Vue logo" :src="url">
    </div>
  </div>
</template>

<script>
import { v4 as uuidv4 } from 'uuid';
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  data:{
	url: 'http://127.0.0.1:8005/api/v1/user/generateQrCode?uuid=' + uuidv4()
  }
  components: {
    HelloWorld
  },
  sockets: {
    connect() {
      console.log('链接成功');
    },
    dataState2: (data) => {
      console.log(data,'data')
    },
    disconnect() {
      console.log('断开链接')
    },
    reconnect() {
      console.log('重新链接')
    },
    sendAccessToken(res) {
      console.log('VueSocketIO', res.accessToken) // 调用用户信息接口
    }
  },
  mounted() {
    console.log(this)
    // this.$socket.emit('chat', { subscribe: true })  // 事件派发
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

就这样基础版本的微信小程序扫码,web自动登录完成

你可能感兴趣的:(egg,Vue,微信开发,小程序,redis,vue,扫码登录)