Blogs AI
蓝绿渐变 · 深色科技风

Vue-Uniapp-Node 实现扫码登录功能

前端
Vue

1. 使用`qrcode`第三库实现二维码的生成。`npm install qrcode` 2. 从后端获取当前二维码唯一id以及设置过期时间等。 3. 二维码生成后开启轮询监听用户是否扫码以及二维码是否过期。 4. 用户扫码登录成功后返回登录信息以及token等。

Vue-Uniapp-Node 实现扫码登录功能

PC端

pc端采用vue组件化形式实现二维码组件。

实现思路:

  1. 使用qrcode第三库实现二维码的生成。npm install qrcode
  2. 从后端获取当前二维码唯一id以及设置过期时间等。
  3. 二维码生成后开启轮询监听用户是否扫码以及二维码是否过期。
  4. 用户扫码登录成功后返回登录信息以及token等。

qrcode.vue

html
<template>
  <div class="qr-code" :style="{ width: size + 'px', height: size + 'px' }">
    <canvas @click="createQRCode" ref="canvas" width="300" height="300"></canvas>
    <div class="mask" v-if="qrcodeStatus !== 1">
      <span class="hint-message" :class="{
        loading: qrcodeStatus === 0,
        error: qrcodeStatus === 2 || qrcodeStatus === 3,
        success: qrcodeStatus === 4
      }">{{ qrcodeStatusName }}</span>
      <span v-if="[2, 3].includes(qrcodeStatus)" class="update-btn" @click="createQRCode">重新加载</span>
    </div>
  </div>
</template>
js
<script>
// 扫码登录功能
import QRCode from "qrcode" // 生成二维码图
import { nanoid } from "nanoid" // 生成唯一id
import { Message } from 'element-ui'
export default {
  name: 'QRCode',
  props: {
    size: {
      type: Number,
      default: 150
    }
  },
  data() {
    return {
      // 0: 生成状态/ 1: 生成成功/ 2:生成失败/
      // 3:二维码过期/ 4:扫码成功/
      qrcodeStatus: 0,
      qrcode: {
        url: window.location.href,
        id: nanoid(), // 唯一id
      }, // 二维码登录信息
      timer: null, // 轮询定时器
    }
  },
  computed: {
    // 状态文字
    qrcodeStatusName() {
      const arr = ['加载中...', '', '加载失败', '二维码过期', '扫码成功√']
      return arr[this.qrcodeStatus]
    }
  },
  mounted() {
    this.createQRCode() // 生成二维码
  },
  beforeDestroy() {
    if (this.timer) {
      clearInterval(this.timer)
    }
  },
  methods: {
    // 生成二维码 (canvas对象)
    async createQRCode() {
      this.qrcodeStatus = 0
      // 获取唯一id
      this.qrcode.id = (await this.$api.login.getQrCode()).data

      // 生成canvas二维码
      QRCode.toCanvas(this.$refs.canvas, JSON.stringify(this.qrcode), {
        width: this.size,
      }, (error) => {
        // 生成错误时
        if (error) {
          this.qrcodeStatus = 2
        }
        else {
          this.qrcodeStatus = 1
          // 开启轮询
          this.sendPoll()
        }
      })
    },
    // 开启轮询是否扫码
    sendPoll() {
      if (this.timer) {
        clearInterval(this.timer)
        this.timer = null
      }
      this.timer = setInterval(() => {
        this.$api.login.sendPoll({ id: this.qrcode.id }).then(res => {
          Message.closeAll()
          // 登录成功
          if (res.code === 200) {
            clearInterval(this.timer)
            this.timer = null
            this.qrcodeStatus === 4
            setTimeout(() => {
              this.$emit('success', res.data)
            }, 300)
          }
          else if (res.code === 202) { // 未登录

          }
          else { // 查询失败或登录过期
            clearInterval(this.timer)
            this.timer = null
            this.qrcodeStatus = 3
          }
        })
      }, 1500)
    }
  }
}
</script>
css
<style lang="less" scoped>
.qr-code {
  position: relative;
  overflow: hidden;
  margin-top: 10px;

  .mask {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(255, 255, 255, .95);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;

    .hint-message {
      font-size: 1rem;
      font-weight: bold;
      font-family: Arial, Helvetica, sans-serif;

      &.loading {
        display: flex;
        flex-direction: column;
        align-items: center;
        font-weight: 500;
        font-size: 0.9rem;
        color: #4c9cff;

        &::before {
          width: 30px;
          height: 30px;
          content: '';
          display: block;
          border: 2px solid #4c9cff;
          border-radius: 50%;
          border-right: none;
          border-top: none;
          border-left-width: 3px;
          border-bottom-width: 2px;
          margin-bottom: 20px;
          animation: ratote linear infinite 1s;

          @keyframes ratote {
            0% {
              transform: rotate(0deg);
            }

            100% {
              transform: rotate(360deg);
            }
          }
        }
      }

      &.error {
        color: #F56C6C;
      }

      &.success {
        color: #67C23A;
      }
    }

    .update-btn {
      color: #4c9cff;
      font-size: 0.8rem;
      margin-top: 10%;
      cursor: pointer;
    }
  }
}
</style>

APP端

app端采用uni-app实现,用户在app上登录并扫描pc端二维码。

实现思路:

  1. 登录app并存储携带token。
  2. 扫码二维码发送登录确认请求。(携带当前账号token信息)
vue
<template>
	<view class="qr-code" @tap="scanCode">
		<uni-icons type="scan" size="24" color="#fff"></uni-icons>
	</view>
</template>

<script setup>
	import { qrCodeLogin } from '@/Api/login.js'
	import { useUserInfoStore } from '@/store/pinia.js'
	import { toRef } from 'vue'
	const store = useUserInfoStore()

	const isLogin = toRef(store, 'isLogin')
	const scanCode = (e) => {
		// #ifdef APP-PLUS
		// 判断是否登录
		if (!isLogin.value) {
			uni.navigateTo({
				url: '/pages/login/login'
			})
			return
		}
		// 使用相机扫码
		uni.scanCode({
			onlyFromCamera: true,
			scanType: "qrCode",
			success({ result }) {
				// 输出扫码结果
				try {
					const { id, url } = JSON.parse(result)
					uni.showModal({
						title: '确认登录',
						content: `正在扫码登录${url},请确认是否本人操作!!!`,
						success(res) {
							// 确认登录
							if (res.confirm) {
								// 发送登录请求
								qrCodeLogin({ id }).then(res => {
									if (res.code === 200) {
										uni.showToast({
											title: res.msg || '登录成功',
											icon: 'success'
										})
									} else {
										uni.showToast({
											title: res.msg || '登录异常',
											icon: 'error'
										})
									}
								})
							}
						}
					})
				} catch (e) {
					console.log(e);
					uni.showToast({
						title: '无法识别二维码',
						icon: 'exception'
					})
					//TODO handle the exception
				}
			}
		})
		// #endif
	}
</script>

<style lang="less">

</style>

服务端

服务器采用Express框架搭建接口,共实现三个接口请求。

  1. 获取二维码唯一id并设置过期时间。
  2. 轮询获取二维码当前状态。
  3. app端确认登录请求。
js
// 扫码数据存储
const { v4: uuid } = require('uuid')
const _QrCode = new Map()

// 获取唯一id和设置过期时间
router.post('/getQrCode', async (req, res) => {
  const date = Date.now() // 当前时间戳
  const data = {
    startTime: date,
    endTime: date + 1000 * 60 * 3, // 三分钟有效时间
    id: uuid(), // 唯一id
    status: false, // 是否扫码
    data: null, // 数据信息
  }
  _QrCode.set(data.id, data)

  // 清理过期数据
  _QrCode.forEach((v, k, m) => {
    if (v.endTime < date) m.delete(k)
  })

  res.$data(200, 'uuid', data.id)
})

// 扫码轮询接口
router.post('/sendPoll', async (req, res) => {
  const { id } = req.body
  if (id) {
    if (_QrCode.has(id) && _QrCode.get(id).endTime > Date.now()) { // 判断二维码是否过期
      const qrcode = _QrCode.get(id)
      if (qrcode.status && qrcode.data) { // 判断是否登录
        _QrCode.delete(id)
        res.$data(200, '登录成功', qrcode.data)
      }
      else {
        res.$data(202, '未登录', null)
      }
    }
    else {
      res.$data(201, '登录过期', null)
    }
  }
  else {
    res.$data(400, '查询失败', 200)
  }
})

// 扫码确认接口
router.post('/qrCodeLogin', [isToken], async (req, res) => {
  const { id } = req.body
  const { username } = req.$token // 获取用户名

  if (id && username) {
    try {
      // 判断是否过期
      const qrcode = _QrCode.get(id)
      if (_QrCode.has(id) && qrcode.endTime > Date.now()) {
        const result = (await loginModel.select({ username }))[0] // 获取用户信息存储
        if (result && !qrcode.status) { // 判断用户是否存在并且防止登录冲突
          // 生成Token
          const token = JWT.generate({
            _id: result._id,
            username: result.username,
            root: result.root
          }, "1d")
          result.token = token
          qrcode.data = result
          qrcode.status = true // 变更状态
          _QrCode.set(id, qrcode)
          res.$data(200, '登录成功', null)
        }
        else {
          res.$data(400, '无权限登录', null)
        }
      }
      else {
        res.$data(400, '验证码过期', null)
      }

    } catch (error) {
      console.log(error)
      res.$data(400, '登录失败', null)
    }
  }
  else {
    res.$data(400, '登录失败', null)
  }
})
Vue-Uniapp-Node 实现扫码登录功能