扫码登录完整实现指南 - 从需求分析到前端实现
前言
扫码登录已成为当前主流应用的标配功能,从微信、钉钉到各种企业应用,这种登录方式既提高了用户体验,又增强了账户安全性。本文将从需求分析开始,详细介绍扫码登录功能的完整实现流程,包括接口设计、前端实现以及状态管理等内容。无论你是否接触过相关业务,阅读本文都能帮助你快速掌握扫码登录的实现要点。
需求分析
首先,我们来理清基本需求:
- 登录二维码有效期默认2分钟,超过2分钟提示用户刷新
- 扫码成功后,跳转扫码成功页面,用户在移动端点击确认登录后,登录进入应用
需求理解
关于二维码有效期: 在生成登录二维码时,后端服务器将生成一个二维码标识符,并设置一个2分钟的有效期。后端可以通过时间戳或其他方法来跟踪二维码的有效性。前端在展示二维码的同时也开始计时,在二维码有效期结束前,如果用户未完成扫码登录,提示用户刷新二维码。
关于扫码登录流程: 一旦用户在移动设备上成功扫描二维码,并点击确认登录按钮,前端应轮询向后端发送请求,确认用户的登录操作。
流程设计
根据需求分析,扫码登录的完整流程如下:
接口设计
根据流程图,我们需要设计以下三个核心接口:
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>
状态管理与用户体验优化
扫码登录过程中有多种状态需要管理,为了提供良好的用户体验,我们需要针对不同状态提供清晰的反馈:
- 等待扫码:展示二维码和引导文案
- 已扫描:提示用户在手机上确认
- 登录成功:展示成功动画,准备跳转
- 二维码过期:提供刷新按钮
- 登录失败:展示错误信息,提供重试选项
以下是一个简化的状态管理代码示例:
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;
// 其他状态处理...
}
};
安全性考虑
在实现扫码登录时,需要注意以下安全性问题:
- 二维码唯一性:每个二维码应该具有唯一标识,且仅一次有效
- 有效期控制:设置合理的有效期,防止二维码被长时间使用
- 防护重放攻击:确保二维码使用后立即失效
- 用户确认机制:在移动端要求用户明确确认登录操作
- 设备信息验证:记录并验证登录设备信息
总结
扫码登录的实现涉及多个环节,包括二维码生成、状态轮询、用户确认等。通过本文介绍的方法,我们可以实现一个完整的扫码登录功能,既满足了用户的便捷需求,又能保证登录的安全性。
在实际开发中,可能还需要根据具体业务需求做更多定制,例如:
- 多端支持(Web、桌面应用、不同移动设备等)
- 授权范围控制
- 登录历史记录
- 异常行为检测
希望本文能对你实现扫码登录功能有所帮助!如有问题或建议,欢迎在评论区留言交流。
参考资料
- OAuth 2.0 协议: oauth.net/2/
- QR Code 规范: www.qrcode.com/en/about/
- Web API - Timers: developer.mozilla.org/en-US/docs/...