本文目录导读:

二维码登录技术原理
二维码登录的核心原理是基于临时凭证和轮询/推送机制的认证流程:
基本流程
- 生成二维码:服务端生成包含唯一标识符(UUID)的二维码,并存储该标识符与登录状态的映射
- 扫码:手机APP扫描二维码,获取其中的标识符
- 确认登录:手机端将用户令牌和标识符发送给服务端
- 验证轮询:PC端持续轮询服务端,检查该标识符对应的登录状态
- 登录成功:服务端返回登录凭证(如JWT Token)
代码实现示例(简化版)
后端实现(Node.js + Express)
const express = require('express');
const crypto = require('crypto');
const QRCode = require('qrcode');
const app = express();
// 存储二维码状态(生产环境应使用Redis)
const qrCodeStore = new Map();
// 1. 生成二维码接口
app.get('/api/qrcode/generate', async (req, res) => {
const qrId = crypto.randomUUID();
// 存储二维码状态
qrCodeStore.set(qrId, {
status: 'pending', // pending, scanned, confirmed, expired
createdAt: Date.now(),
userId: null
});
// 生成二维码(这里使用临时令牌作为二维码内容)
const qrContent = JSON.stringify({
type: 'login',
qrId: qrId,
timestamp: Date.now()
});
// 生成二维码图片(Base64格式)
const qrCodeDataUrl = await QRCode.toDataURL(qrContent);
res.json({
success: true,
qrId: qrId,
qrCode: qrCodeDataUrl,
expiresIn: 300 // 5分钟过期
});
});
// 2. 扫码确认接口(手机端调用)
app.post('/api/qrcode/scan', (req, res) => {
const { qrId, userId } = req.body;
const qrData = qrCodeStore.get(qrId);
if (!qrData) {
return res.status(404).json({ success: false, message: '二维码已过期' });
}
if (qrData.status !== 'pending') {
return res.status(400).json({ success: false, message: '二维码已被扫描' });
}
// 更新状态为已扫描(手机APP已识别)
qrData.status = 'scanned';
qrData.userId = userId;
qrCodeStore.set(qrId, qrData);
res.json({ success: true, message: '扫码成功' });
});
// 3. 确认登录接口(手机端确认登录后调用)
app.post('/api/qrcode/confirm', (req, res) => {
const { qrId, token } = req.body;
const qrData = qrCodeStore.get(qrId);
if (!qrData || qrData.status !== 'scanned') {
return res.status(400).json({ success: false });
}
// 生成JWT Token或其他登录凭证
const loginToken = generateJWT(qrData.userId);
// 更新状态为已确认
qrData.status = 'confirmed';
qrData.loginToken = loginToken;
qrCodeStore.set(qrId, qrData);
res.json({ success: true });
});
// 4. 轮询接口(PC端调用,检查登录状态)
app.get('/api/qrcode/status/:qrId', (req, res) => {
const { qrId } = req.params;
const qrData = qrCodeStore.get(qrId);
if (!qrData) {
return res.json({ status: 'expired', message: '二维码已过期' });
}
// 检查是否过期(5分钟)
if (Date.now() - qrData.createdAt > 300000) {
qrData.status = 'expired';
qrCodeStore.set(qrId, qrData);
return res.json({ status: 'expired', message: '二维码已过期' });
}
// 根据不同状态返回
const response = {
status: qrData.status,
message: ''
};
switch(qrData.status) {
case 'pending':
response.message = '等待扫描';
break;
case 'scanned':
response.message = '已扫描,等待确认';
break;
case 'confirmed':
response.message = '登录成功';
response.token = qrData.loginToken;
break;
}
res.json(response);
});
// 辅助函数:生成JWT
function generateJWT(userId) {
// 实际使用jsonwebtoken库
return `jwt_${userId}_${Date.now()}`;
}
app.listen(3000, () => {
console.log('二维码登录服务运行在端口3000');
});
前端实现(Vue.js示例)
<template>
<div class="qr-login">
<div v-if="loading">
<img :src="qrCodeUrl" alt="二维码" />
<p>请使用手机APP扫描二维码</p>
<p>状态:{{ statusText }}</p>
</div>
<div v-else-if="error">
<p>二维码加载失败:{{ error }}</p>
<button @click="generateQR">重新获取</button>
</div>
<div v-else-if="loggedIn">
<p>登录成功!欢迎回来</p>
<p>Token: {{ token }}</p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
qrId: '',
qrCodeUrl: '',
status: 'pending',
token: '',
loading: false,
error: '',
pollTimer: null
}
},
computed: {
statusText() {
const map = {
'pending': '等待扫描',
'scanned': '已扫描,请在手机上确认',
'confirmed': '登录成功',
'expired': '二维码已过期'
}
return map[this.status] || '未知状态'
}
},
methods: {
async generateQR() {
this.loading = true
this.error = ''
try {
const response = await fetch('/api/qrcode/generate')
const data = await response.json()
this.qrId = data.qrId
this.qrCodeUrl = data.qrCode
this.status = 'pending'
// 开始轮询
this.startPolling()
} catch (err) {
this.error = err.message
} finally {
this.loading = false
}
},
startPolling() {
this.stopPolling()
this.pollTimer = setInterval(async () => {
try {
const response = await fetch(`/api/qrcode/status/${this.qrId}`)
const data = await response.json()
this.status = data.status
if (data.status === 'confirmed') {
this.token = data.token
this.stopPolling()
} else if (data.status === 'expired') {
this.stopPolling()
this.error = '二维码已过期,请刷新页面重新获取'
}
} catch (err) {
console.error('轮询失败:', err)
}
}, 2000) // 每2秒轮询一次
},
stopPolling() {
if (this.pollTimer) {
clearInterval(this.pollTimer)
this.pollTimer = null
}
}
},
mounted() {
this.generateQR()
},
beforeDestroy() {
this.stopPolling()
}
}
</script>
安全考虑
- :使用随机生成的UUID,避免包含敏感信息
- 过期机制:设置合理的过期时间(通常5分钟)
- 一次性使用:扫码后状态立即变更,防止重复使用
- HTTPS传输:所有通信使用加密连接
- 防篡改:对关键数据进行签名验证
- 频率限制:对扫码和确认接口做频率限制,防止暴力攻击
- Token安全:使用JWT等安全的令牌格式,并设置合理的有效期
生产环境优化
- 使用Redis存储二维码状态,支持分布式部署
- 使用WebSocket代替轮询,提高实时性
- 添加设备信息和地理位置验证,增强安全性
- 实现二维码防重复扫描机制
- 添加日志记录和异常监控
这个实现涵盖了二维码登录的核心流程,实际生产中还需要根据具体业务需求进行调整和优化。