扫码登陆实现流程及示例Demo展示

文章说明

本文主要为了学习 APP扫码登陆实现登陆效果的原理,该方法主要适用于 Web 端与APP端共用一个身份验证授权系统的场景;借APP端的身份以及二维码这个媒介,来实现给Web端授权的效果

比较有难度的几个小点:
1、登录流程的理解
2、如何实现APP扫描二维码
3、二维码里面放什么内容

登录流程介绍

1、APP端需要是登陆状态
2、Web端进行登陆界面,选择二维码扫描方式登录
3、后端生成登录二维码,注意,该二维码有一个唯一的二维码ID,且具有过期时间,暂定为3分钟;在Web、端二维码生成后的3分钟内,会进行轮询,来判断二维码是否已被使用且登陆成功
4、用户拿出手机端的APP,已登录后有扫码登陆的功能,扫码登陆即可,此时扫码后会弹出是否确认登陆的选项,用户选择确认登陆
5、用户确认登陆后,APP端会将二维码ID和当前的用户信息发送到授权中心,授权中心会通过APP端的登录授权来生成一个临时token,同时绑定二维码ID,将二维码状态变为已扫码
6、第3步中轮询二维码状态的操作会被监听到,二维码状态变为已扫码后,此时Web端的二维码ID派上用场,Web端拿着这个二维码ID去拿到一个临时token
7、Web端拿到临时token后,会采用这个临时token去向授权中心申请一个较长时间的接口token,这个token可以进行后续的接口访问;当然这类token都是有时限的,暂且定为3小时

采用Hutool工具包生成二维码

添加依赖

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>4.6.10</version>
</dependency>

<dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>core</artifactId>
    <version>3.3.3</version>
</dependency>

生成二维码,代码非常简单方便;当content为一个链接时,扫码后会自动跳转到该链接;当然本文的APP扫码,是设置为一个登陆页面的路由

    @GetMapping("/generateQrCode")
    public void generateQrCode(@RequestParam String content, HttpServletResponse response) throws Exception {
        QrCodeUtil.generate(content, 400, 400, "png", response.getOutputStream());
    }

采用Vue扫码

参考文章–Vue 移动端实现调用相机扫描二维码或条形码
不过要求电脑或者手机有摄像头才行,不然 codeReader.listVideoInputDevices() 获取到的对象长度为0,后续流程进行不了

<template>
  <div class="scan-page">
    <video ref="video" id="video" class="scan-video" autoplay></video>
    <div v-show="data.tipShow" class="scan-tip">{{ data.tipMsg }}</div>
  </div>
</template>

<script>
import {BrowserMultiFormatReader} from '@zxing/library';
import {onMounted, reactive} from "vue";

export default {
  setup() {
    const data = reactive({
      tipShow: false,
      tipMsg: '',
      scanText: '',
    });

    onMounted(() => {
      openScan();
    });

    let codeReader;

    async function openScan() {
      codeReader = await new BrowserMultiFormatReader();
      codeReader.listVideoInputDevices().then(videoDevices => {
        data.tipMsg = '正在调用摄像头...';
        data.tipShow = true;
        console.log('get-videoDevices', videoDevices);

        let firstDeviceId = videoDevices[videoDevices.length - 1].deviceId;

        if (videoDevices.length > 1) {
          firstDeviceId = videoDevices.find(el => {
            return el.label.indexOf('back') > -1 && el.label.indexOf('0') > -1
          }) ? videoDevices.find(el => {
            return el.label.indexOf('back') > -1 && el.label.indexOf('0') > -1
          }).deviceId : videoDevices[videoDevices.length - 1].deviceId;
        }
        console.log('get-firstDeviceId', firstDeviceId);

        decodeFromInputVideoFunc(firstDeviceId);
      }).catch(err => {
        data.tipShow = false;
        console.error(err);
      });
    }

    function decodeFromInputVideoFunc(firstDeviceId) {
      codeReader.reset();
      codeReader.decodeFromInputVideoDeviceContinuously(firstDeviceId, 'video', (result) => {
        data.tipMsg = '正在尝试识别...';
        if (result) {
          console.log('扫码结果', result);
        }
      });
    }

    return {
      data,
    }
  },
}
</script>

<style scoped>
.scan-page {
  min-height: 100vh;
  background-color: #363636;
  overflow-y: hidden;
}

.scan-video {
  height: 85vh;
}

.scan-tip {
  width: 100vw;
  text-align: center;
  color: white;
  font-size: 5vw;
}
</style>

简单版

由于台式机的摄像头问题,我暂时写一个简单的流程模拟版,后续补充

当前效果如下:
在这里插入图片描述
在这里插入图片描述

这里没有进行很多安全校验,会有一些问题,后续可以补充安全校验;

前端代码:App.vue

<template>
  <h2 v-if="data.isLogin">当前用户身份:{{ data.username + "/" + data.password }}</h2>
  <h2 v-if="!data.isLogin">请先扫码登录</h2>

  <img alt="" src="" id="qrCode" @click="changeQrCode"/>
  <h4>二维码倒计时:{{ data.validTime + "秒" }} <span style="margin-left: 30px">点击可刷新二维码</span></h4>

  <h4 style="margin-top: 30px; cursor: pointer" @click="appEnsure">模拟手机端点击确认</h4>
</template>

<script>
import {onMounted, reactive} from "vue";
import {getRequest, postRequest} from "@/util";

export default {
  setup() {
    const data = reactive({
      username: '',
      password: '',
      isLogin: false,
      validTime: 300,
    });

    onMounted(() => {
      changeQrCode();
    });

    function genUUID() {
      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        const r = (Math.random() * 16) | 0, v = c === 'x' ? r : (r & 0x3) | 0x8;
        return v.toString(16);
      });
    }

    let timer;
    let uuid;

    function changeQrCode() {
      if (timer) {
        clearInterval(timer);
        data.validTime = 300;
      }

      const qrCode = document.getElementById("qrCode");
      uuid = genUUID();
      qrCode.src = "http://127.0.0.1:8081/login/generateQrCode?uuid=" + uuid;
      timer = setInterval(() => {
        data.validTime--;
        if (data.validTime === 0) {
          clearInterval(timer);
          console.log("二维码已过期");
        }

        getRequest("/login/validScan?uuid=" + uuid).then((tempTokenRes) => {
          if (tempTokenRes.data.code === 200) {
            clearInterval(timer);

            getRequest("/login/getNewToken?tempToken=" + tempTokenRes.data.data).then((newTokenRes) => {
              if (newTokenRes.data.code === 200) {
                data.isLogin = true;
                console.log("登陆成功");
                data.username = newTokenRes.data.data[0];
                data.password = newTokenRes.data.data[1];
              }
            });
          }
        })
      }, 1000);
    }

    function appEnsure() {
      if (!uuid) {
        console.log("当前还未生成二维码");
        return
      }

      postRequest("/login/appEnsure", {
        uuid: uuid,
        username: "BBYH",
        password: "1234",
      }).then((res) => {
        console.log(res);
      });
    }

    return {
      data,
      changeQrCode,
      appEnsure,
    }
  },
}
</script>

<style scoped>
* {
  margin: 0;
  padding: 0;
  text-align: center;
  user-select: none;
}

h2 {
  background-color: orange;
  padding: 10px;
  height: 50px;
  line-height: 50px;
}

img {
  width: 400px;
  height: 400px;
  display: block;
  margin: 40px auto;
  cursor: pointer;
}

h4 {
  background-color: orangered;
  padding: 10px;
  height: 50px;
  line-height: 50px;
}
</style>

后端代码:LoginController.java

package com.boot.controller;

import cn.hutool.core.lang.UUID;
import cn.hutool.extra.qrcode.QrCodeUtil;
import com.boot.entity.Result;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * @author bbyh
 * @date 2023/12/23 11:43
 */
@RestController
@RequestMapping("/login")
public class LoginController {
    private static final Map<String, String[]> USER_MAP = new HashMap<>(4);
    private static final Map<String, String> TEMP_TOKEN_MAP = new HashMap<>(4);
    private static final Map<String, Integer> QR_MAP = new HashMap<>(4);
    private static final String CONTENT_URL = "http://127.0.0.1:8080/#/login?uuid=";

    private static final Integer NOT_SCAN = 0;
    private static final Integer SCANNED = 1;

    @GetMapping("/generateQrCode")
    public void generateQrCode(@RequestParam String uuid, HttpServletResponse response) throws Exception {
        QR_MAP.put(uuid, NOT_SCAN);
        QrCodeUtil.generate(CONTENT_URL + uuid, 400, 400, "png", response.getOutputStream());
    }

    @GetMapping("/validScan")
    public Result validScan(@RequestParam String uuid) {
        if (Objects.equals(QR_MAP.get(uuid), NOT_SCAN)) {
            return Result.error("当前二维码未被扫描", null);
        } else if (Objects.equals(QR_MAP.get(uuid), SCANNED)) {
            return Result.success("当前二维码被成功扫描", TEMP_TOKEN_MAP.get(uuid));
        } else {
            return Result.error("当前二维码ID不存在", null);
        }
    }

    @PostMapping("appEnsure")
    public Result appEnsure(@RequestBody Map<String, String> map) {
        String uuid = map.get("uuid");
        String token = UUID.randomUUID().toString();
        TEMP_TOKEN_MAP.put(uuid, token);

        String username = map.get("username");
        String password = map.get("password");
        USER_MAP.put(token, new String[]{username, password});

        QR_MAP.put(uuid, SCANNED);
        return Result.success("APP端点击确认", null);
    }

    @GetMapping("/getNewToken")
    public Result getNewToken(@RequestParam String tempToken) {
        if (USER_MAP.get(tempToken) != null) {
            return Result.success("登录成功", USER_MAP.get(tempToken));
        }
        return Result.error("临时token:" + tempToken + "不存在", null);
    }

}

后端需要加一个跨域配置

package com.boot.config;

import com.sun.istack.internal.NotNull;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author bbyh
 * @date 2023/11/19 12:32
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(@NotNull CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                .maxAge(3600);
    }
}

相对完整版

上述的简单页面最初只为了简单模拟扫码登陆的后端、Web端、APP端的流程;目前我添加了一个较为完整的界面来演示这个扫码登陆的效果;不过由于我目前的摄像头组件的缺失,扫码功能无法继续完成了;不过目前其余部分的功能都测试好了,效果展示如下:

页面是参考新版QQ的登录页面来书写的,仅供学习使用

Web扫码登陆
在这里插入图片描述

Web登陆后的信息页面
在这里插入图片描述

APP账密登录
在这里插入图片描述

APP登录后的信息页面
在这里插入图片描述

APP摄像头扫码页面
在这里插入图片描述

代码下载

扫码登陆示例demo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值