扫码登录完整实现指南 - 从需求分析到前端实现

扫码登录完整实现指南 - 从需求分析到前端实现

前言

扫码登录已成为当前主流应用的标配功能,从微信、钉钉到各种企业应用,这种登录方式既提高了用户体验,又增强了账户安全性。本文将从需求分析开始,详细介绍扫码登录功能的完整实现流程,包括接口设计、前端实现以及状态管理等内容。无论你是否接触过相关业务,阅读本文都能帮助你快速掌握扫码登录的实现要点。

需求分析

首先,我们来理清基本需求:

  • 登录二维码有效期默认2分钟,超过2分钟提示用户刷新
  • 扫码成功后,跳转扫码成功页面,用户在移动端点击确认登录后,登录进入应用

需求理解

关于二维码有效期: 在生成登录二维码时,后端服务器将生成一个二维码标识符,并设置一个2分钟的有效期。后端可以通过时间戳或其他方法来跟踪二维码的有效性。前端在展示二维码的同时也开始计时,在二维码有效期结束前,如果用户未完成扫码登录,提示用户刷新二维码。

关于扫码登录流程: 一旦用户在移动设备上成功扫描二维码,并点击确认登录按钮,前端应轮询向后端发送请求,确认用户的登录操作。

流程设计

根据需求分析,扫码登录的完整流程如下:

sequenceDiagram participant PC as PC端 participant Server as 服务端 participant Mobile as 移动端 PC->>Server: 请求生成二维码 Server-->>PC: 返回二维码数据 Note over PC: 展示二维码,开始轮询查询状态 loop 每隔几秒 PC->>Server: 查询二维码状态 Server-->>PC: 返回状态(未扫描) end Mobile->>PC: 扫描二维码 Mobile->>Server: 发送扫码确认请求 Server-->>Mobile: 返回确认结果 PC->>Server: 查询二维码状态 Server-->>PC: 返回状态(已扫描) Mobile->>Server: 确认登录 Server-->>Mobile: 确认成功 PC->>Server: 查询二维码状态 Server-->>PC: 返回状态(已确认) Note over PC: 登录成功,进入应用

接口设计

根据流程图,我们需要设计以下三个核心接口:

1. 二维码生成接口

接口命名: POST: /v1/accounts/qrcode/

请求参数:

typescript 复制代码
export interface Request {
    /**
     * 重新生成/取消时需要给
     */
    code?: string;
    platform: string;
}

平台类型:

arduino 复制代码
GKOL_PC      // xx PC端
GKOL_WEB     // xx Web端
GKOL_WEB_MANAGER   // xx管理端
GKOL_WEB_OPERATE   // xx后台
GKOL_WEB_ORGANIZE  // xx后台

响应结构:

typescript 复制代码
export interface Response {
    /**
     * 二维码,前缀+base64串,格式:rk://scanforpclogin/{qrcode}
     * 二维码json解析结构
     * {
     *   "id":"",
     *   "expire":1685696389,
     *   "prefix":"rk://scanforpclogin/"
     *   "platform":"Rxx_PC"
     * }
     */
    png: string;
}

2. 扫描二维码接口

接口命名: POST: /v1/accounts/qrcode_fill

说明: 主要给移动端用来扫码调用。移动端先扫码web端的二维码图片,解析得到id,然后将id作为参数传入该接口。

请求参数: 可以省略,用户信息放在header的token中

响应结构:

typescript 复制代码
export interface Response {
    /**
     * 身份卡ID,为空就不限制
     */
    card_id: string;
    /**
     * 二维码令牌,二维码解析结果
     */
    id: string;
    /**
     * 状态,SCAN 已扫描
     * VERIFY 已确认
     * CANCEL 取消
     */
    step: string;
}

3. 账号登录状态查询接口

接口命名: POST /v1/passport/guest

说明: Web端通过轮询该接口,监听移动端的扫码状态。

请求参数:

typescript 复制代码
export interface Request {
    /**
     * 二维码唯一标识
     */
    code: string;
    /**
     * 登录类型,QRCODE表示二维码登录
     */
    type: string;
}

响应结构:

typescript 复制代码
export interface Response {
    /**
     * 登录令牌
     */
    token: string;
    /**
     * 状态码
     * QRCODE_SUCCESS: 成功
     * QRCODE_ERROR: 错误
     * QRCODE_EXPIRE: 过期
     */
    reason: string;
    /**
     * 用户信息
     */
    user_info: UserInfo;
}

前端实现

1. 生成并展示二维码

首先,调用二维码生成接口,然后解析返回的base64数据:

javascript 复制代码
import qrcodeParser from 'qrcode-parser';

const generateQRCode = async () => {
    try {
        const response = await V1CreateQRcode({
            platform: 'GKOL_PC'
        });
        
        if (response.data?.png) {
            // 设置二维码图片URL
            qrcodeUrl.value = `data:image/png;base64,${response.data.png}`;
            
            // 解析二维码内容
            const result = await qrcodeParser(response.data.png);
            QrCodeData.value = result ? JSON.parse(result) : null;
            
            console.log('二维码数据:', QrCodeData.value);
            // {
            //    "id": "unique-identifier", 
            //    "expire": 1685696389,
            //    "prefix": "rk://scanforpclogin/",
            //    "platform": "Rxx_PC"
            // }
            
            // 开始监听扫码状态
            startPolling();
        }
    } catch (error) {
        console.error('生成二维码失败:', error);
    }
};

2. 封装轮询Hook

为了方便状态管理,我们可以封装一个轮询的自定义Hook:

typescript 复制代码
import { ref, onUnmounted } from 'vue';

export function useQRCodePolling() {
    const pollingStatus = ref('WAITING'); // 初始状态:等待扫码
    const pollingTimer = ref(null);
    const qrCodeExpired = ref(false);
    
    const startPolling = (qrCodeId) => {
        if (pollingTimer.value) clearInterval(pollingTimer.value);
        
        // 计算过期时间
        const expireTime = Date.now() + 2 * 60 * 1000; // 2分钟后过期
        
        pollingTimer.value = setInterval(async () => {
            try {
                // 检查是否过期
                if (Date.now() > expireTime) {
                    clearInterval(pollingTimer.value);
                    qrCodeExpired.value = true;
                    return;
                }
                
                // 调用查询接口
                const response = await V1PassportGuest({
                    code: qrCodeId,
                    type: 'QRCODE'
                });
                
                // 根据返回状态处理
                switch(response.data?.reason) {
                    case 'QRCODE_SUCCESS':
                        pollingStatus.value = 'SUCCESS';
                        clearInterval(pollingTimer.value);
                        // 处理登录成功
                        handleLoginSuccess(response.data);
                        break;
                    case 'QRCODE_ERROR':
                        pollingStatus.value = 'ERROR';
                        clearInterval(pollingTimer.value);
                        // 处理错误
                        break;
                    case 'QRCODE_EXPIRE':
                        pollingStatus.value = 'EXPIRED';
                        clearInterval(pollingTimer.value);
                        qrCodeExpired.value = true;
                        // 处理过期
                        break;
                    default:
                        // 继续等待
                        break;
                }
            } catch (error) {
                console.error('轮询出错:', error);
            }
        }, 2000); // 每2秒轮询一次
    };
    
    const refreshQRCode = async () => {
        qrCodeExpired.value = false;
        await generateQRCode(); // 重新生成二维码
    };
    
    // 组件卸载时清除定时器
    onUnmounted(() => {
        if (pollingTimer.value) {
            clearInterval(pollingTimer.value);
        }
    });
    
    return {
        pollingStatus,
        qrCodeExpired,
        startPolling,
        refreshQRCode
    };
}

3. 完整的二维码登录组件

vue 复制代码
<template>
  <div class="qrcode-login-container">
    <div class="qrcode-box">
      <div v-if="!qrCodeExpired" class="qrcode-image">
        <img :src="qrcodeUrl" alt="登录二维码" />
      </div>
      <div v-else class="qrcode-expired">
        <p>二维码已过期</p>
        <button @click="refreshQRCode">刷新二维码</button>
      </div>
      
      <div class="qrcode-status">
        <p v-if="pollingStatus === 'WAITING'">请使用APP扫描二维码登录</p>
        <p v-if="pollingStatus === 'SCANNED'">已扫描,请在手机上确认</p>
        <p v-if="pollingStatus === 'SUCCESS'">登录成功,正在跳转...</p>
        <p v-if="pollingStatus === 'ERROR'">登录失败,请重试</p>
        <p v-if="pollingStatus === 'EXPIRED'">二维码已过期,请刷新</p>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import qrcodeParser from 'qrcode-parser';
import { V1CreateQRcode, V1PassportGuest } from '@/api/auth';
import { useQRCodePolling } from '@/hooks/useQRCodePolling';

const router = useRouter();
const qrcodeUrl = ref('');
const QrCodeData = ref(null);

const {
  pollingStatus,
  qrCodeExpired,
  startPolling,
  refreshQRCode: refreshQRCodeState
} = useQRCodePolling();

// 生成二维码
const generateQRCode = async () => {
  try {
    const response = await V1CreateQRcode({
      platform: 'GKOL_PC'
    });
    
    if (response.data?.png) {
      qrcodeUrl.value = `data:image/png;base64,${response.data.png}`;
      
      const result = await qrcodeParser(response.data.png);
      QrCodeData.value = result ? JSON.parse(result) : null;
      
      if (QrCodeData.value?.id) {
        startPolling(QrCodeData.value.id);
      }
    }
  } catch (error) {
    console.error('生成二维码失败:', error);
  }
};

// 刷新二维码
const refreshQRCode = async () => {
  await refreshQRCodeState();
  await generateQRCode();
};

// 处理登录成功
const handleLoginSuccess = (data) => {
  // 保存token
  localStorage.setItem('token', data.token);
  
  // 存储用户信息
  if (data.user_info) {
    localStorage.setItem('userInfo', JSON.stringify(data.user_info));
  }
  
  // 延迟跳转,给用户一个视觉反馈
  setTimeout(() => {
    router.push('/dashboard');
  }, 1000);
};

onMounted(() => {
  generateQRCode();
});
</script>

<style scoped>
.qrcode-login-container {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 40px;
}

.qrcode-box {
  text-align: center;
  background: #fff;
  padding: 30px;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.qrcode-image img {
  width: 200px;
  height: 200px;
}

.qrcode-expired {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 200px;
  height: 200px;
  background: #f5f5f5;
  border-radius: 4px;
}

.qrcode-expired button {
  margin-top: 16px;
  padding: 8px 16px;
  background: #1890ff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.qrcode-status {
  margin-top: 16px;
  font-size: 14px;
  color: #666;
}
</style>

状态管理与用户体验优化

扫码登录过程中有多种状态需要管理,为了提供良好的用户体验,我们需要针对不同状态提供清晰的反馈:

  1. 等待扫码:展示二维码和引导文案
  2. 已扫描:提示用户在手机上确认
  3. 登录成功:展示成功动画,准备跳转
  4. 二维码过期:提供刷新按钮
  5. 登录失败:展示错误信息,提供重试选项

以下是一个简化的状态管理代码示例:

typescript 复制代码
// 状态定义
enum QRCodeStatus {
  WAITING = 'WAITING',
  SCANNED = 'SCANNED',
  SUCCESS = 'SUCCESS',
  ERROR = 'ERROR',
  EXPIRED = 'EXPIRED'
}

// 状态对应的UI提示
const statusMessages = {
  [QRCodeStatus.WAITING]: '请使用APP扫描二维码登录',
  [QRCodeStatus.SCANNED]: '已扫描,请在手机上确认',
  [QRCodeStatus.SUCCESS]: '登录成功,正在跳转...',
  [QRCodeStatus.ERROR]: '登录失败,请重试',
  [QRCodeStatus.EXPIRED]: '二维码已过期,请刷新'
};

// 状态转换处理
const handleStatusChange = (status: QRCodeStatus) => {
  currentStatus.value = status;
  
  switch(status) {
    case QRCodeStatus.SUCCESS:
      // 播放成功动画
      playSuccessAnimation();
      // 延迟跳转
      setTimeout(() => {
        router.push('/dashboard');
      }, 1500);
      break;
      
    case QRCodeStatus.EXPIRED:
      // 显示刷新按钮
      showRefreshButton.value = true;
      break;
      
    // 其他状态处理...
  }
};

安全性考虑

在实现扫码登录时,需要注意以下安全性问题:

  1. 二维码唯一性:每个二维码应该具有唯一标识,且仅一次有效
  2. 有效期控制:设置合理的有效期,防止二维码被长时间使用
  3. 防护重放攻击:确保二维码使用后立即失效
  4. 用户确认机制:在移动端要求用户明确确认登录操作
  5. 设备信息验证:记录并验证登录设备信息

总结

扫码登录的实现涉及多个环节,包括二维码生成、状态轮询、用户确认等。通过本文介绍的方法,我们可以实现一个完整的扫码登录功能,既满足了用户的便捷需求,又能保证登录的安全性。

在实际开发中,可能还需要根据具体业务需求做更多定制,例如:

  • 多端支持(Web、桌面应用、不同移动设备等)
  • 授权范围控制
  • 登录历史记录
  • 异常行为检测

希望本文能对你实现扫码登录功能有所帮助!如有问题或建议,欢迎在评论区留言交流。

参考资料

  1. OAuth 2.0 协议: oauth.net/2/
  2. QR Code 规范: www.qrcode.com/en/about/
  3. Web API - Timers: developer.mozilla.org/en-US/docs/...
相关推荐
李慕婉学姐1 天前
基于微信小程序的运动会信息管理系统k6kqgy34(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·微信小程序·小程序
咸虾米_1 天前
uniapp+unicloud实战项目,九两酒微信小程序商城及后台管理系统前后端部署运行步骤
微信小程序·uni-app·uniapp实战项目·unicloud云开发·vue3后台管理
阿里巴巴AI编程社区2 天前
用Qoder打造自己的AI工作台,普通人也可10倍提效!
微信小程序
wangpq2 天前
记录曾经打开半屏小程序遇到的事
前端·微信小程序
jay神2 天前
【原创】基于小程序的图书馆座位预约系统
微信小程序·小程序·毕业设计·图书馆自习室座位预约系统·座位预约系统
计算机徐师兄2 天前
Java基于微信小程序的物流管理系统【附源码、文档说明】
java·微信小程序·物流管理系统·java物流管理系统小程序·物流管理系统小程序·物流管理系统微信小程序·java物流管理系统微信小程序
云起SAAS3 天前
倒班日历助手抖音快手微信小程序看广告流量主开源
微信小程序·小程序·ai编程·看广告变现轻·倒班日历助手
sheji34163 天前
【开题答辩全过程】以 基于微信小程序的失物认领系统为例,包含答辩的问题和答案
微信小程序·小程序
计算机毕设指导63 天前
基于微信小程序的网络安全知识科普平台系统【源码文末联系】
java·spring boot·安全·web安全·微信小程序·小程序·tomcat
toooooop85 天前
微信小程序轮播图高度自适应优化
微信小程序·小程序